Merge branch 'master' into type-portal-features-store
This commit is contained in:
commit
80e2c6e561
|
@ -10,7 +10,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"bulma": "^0.9.3",
|
||||
"next": "14.2.15",
|
||||
"next": "14.2.21",
|
||||
"node-fetch": "^3.2.10",
|
||||
"sass": "^1.52.3",
|
||||
"react": "17.0.2",
|
||||
|
|
|
@ -46,10 +46,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
|
||||
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
|
||||
|
||||
"@next/env@14.2.15":
|
||||
version "14.2.15"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.15.tgz#06d984e37e670d93ddd6790af1844aeb935f332f"
|
||||
integrity sha512-S1qaj25Wru2dUpcIZMjxeMVSwkt8BK4dmWHHiBuRstcIyOsMapqT4A4jSB6onvqeygkSSmOkyny9VVx8JIGamQ==
|
||||
"@next/env@14.2.21":
|
||||
version "14.2.21"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.21.tgz#09ff0813d29c596397e141205d4f5fd5c236bdd0"
|
||||
integrity sha512-lXcwcJd5oR01tggjWJ6SrNNYFGuOOMB9c251wUNkjCpkoXOPkDeF/15c3mnVlBqrW4JJXb2kVxDFhC4GduJt2A==
|
||||
|
||||
"@next/eslint-plugin-next@12.1.0":
|
||||
version "12.1.0"
|
||||
|
@ -58,50 +58,50 @@
|
|||
dependencies:
|
||||
glob "7.1.7"
|
||||
|
||||
"@next/swc-darwin-arm64@14.2.15":
|
||||
version "14.2.15"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.15.tgz#6386d585f39a1c490c60b72b1f76612ba4434347"
|
||||
integrity sha512-Rvh7KU9hOUBnZ9TJ28n2Oa7dD9cvDBKua9IKx7cfQQ0GoYUwg9ig31O2oMwH3wm+pE3IkAQ67ZobPfEgurPZIA==
|
||||
"@next/swc-darwin-arm64@14.2.21":
|
||||
version "14.2.21"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.21.tgz#32a31992aace1440981df9cf7cb3af7845d94fec"
|
||||
integrity sha512-HwEjcKsXtvszXz5q5Z7wCtrHeTTDSTgAbocz45PHMUjU3fBYInfvhR+ZhavDRUYLonm53aHZbB09QtJVJj8T7g==
|
||||
|
||||
"@next/swc-darwin-x64@14.2.15":
|
||||
version "14.2.15"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.15.tgz#b7baeedc6a28f7545ad2bc55adbab25f7b45cb89"
|
||||
integrity sha512-5TGyjFcf8ampZP3e+FyCax5zFVHi+Oe7sZyaKOngsqyaNEpOgkKB3sqmymkZfowy3ufGA/tUgDPPxpQx931lHg==
|
||||
"@next/swc-darwin-x64@14.2.21":
|
||||
version "14.2.21"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.21.tgz#5ab4b3f6685b6b52f810d0f5cf6e471480ddffdb"
|
||||
integrity sha512-TSAA2ROgNzm4FhKbTbyJOBrsREOMVdDIltZ6aZiKvCi/v0UwFmwigBGeqXDA97TFMpR3LNNpw52CbVelkoQBxA==
|
||||
|
||||
"@next/swc-linux-arm64-gnu@14.2.15":
|
||||
version "14.2.15"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.15.tgz#fa13c59d3222f70fb4cb3544ac750db2c6e34d02"
|
||||
integrity sha512-3Bwv4oc08ONiQ3FiOLKT72Q+ndEMyLNsc/D3qnLMbtUYTQAmkx9E/JRu0DBpHxNddBmNT5hxz1mYBphJ3mfrrw==
|
||||
"@next/swc-linux-arm64-gnu@14.2.21":
|
||||
version "14.2.21"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.21.tgz#8a0e1fa887aef19ca218af2af515d0a5ee67ba3f"
|
||||
integrity sha512-0Dqjn0pEUz3JG+AImpnMMW/m8hRtl1GQCNbO66V1yp6RswSTiKmnHf3pTX6xMdJYSemf3O4Q9ykiL0jymu0TuA==
|
||||
|
||||
"@next/swc-linux-arm64-musl@14.2.15":
|
||||
version "14.2.15"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.15.tgz#30e45b71831d9a6d6d18d7ac7d611a8d646a17f9"
|
||||
integrity sha512-k5xf/tg1FBv/M4CMd8S+JL3uV9BnnRmoe7F+GWC3DxkTCD9aewFRH1s5rJ1zkzDa+Do4zyN8qD0N8c84Hu96FQ==
|
||||
"@next/swc-linux-arm64-musl@14.2.21":
|
||||
version "14.2.21"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.21.tgz#ddad844406b42fa8965fe11250abc85c1fe0fd05"
|
||||
integrity sha512-Ggfw5qnMXldscVntwnjfaQs5GbBbjioV4B4loP+bjqNEb42fzZlAaK+ldL0jm2CTJga9LynBMhekNfV8W4+HBw==
|
||||
|
||||
"@next/swc-linux-x64-gnu@14.2.15":
|
||||
version "14.2.15"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.15.tgz#5065db17fc86f935ad117483f21f812dc1b39254"
|
||||
integrity sha512-kE6q38hbrRbKEkkVn62reLXhThLRh6/TvgSP56GkFNhU22TbIrQDEMrO7j0IcQHcew2wfykq8lZyHFabz0oBrA==
|
||||
"@next/swc-linux-x64-gnu@14.2.21":
|
||||
version "14.2.21"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.21.tgz#db55fd666f9ba27718f65caa54b622a912cdd16b"
|
||||
integrity sha512-uokj0lubN1WoSa5KKdThVPRffGyiWlm/vCc/cMkWOQHw69Qt0X1o3b2PyLLx8ANqlefILZh1EdfLRz9gVpG6tg==
|
||||
|
||||
"@next/swc-linux-x64-musl@14.2.15":
|
||||
version "14.2.15"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.15.tgz#3c4a4568d8be7373a820f7576cf33388b5dab47e"
|
||||
integrity sha512-PZ5YE9ouy/IdO7QVJeIcyLn/Rc4ml9M2G4y3kCM9MNf1YKvFY4heg3pVa/jQbMro+tP6yc4G2o9LjAz1zxD7tQ==
|
||||
"@next/swc-linux-x64-musl@14.2.21":
|
||||
version "14.2.21"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.21.tgz#dddb850353624efcd58c4c4e30ad8a1aab379642"
|
||||
integrity sha512-iAEBPzWNbciah4+0yI4s7Pce6BIoxTQ0AGCkxn/UBuzJFkYyJt71MadYQkjPqCQCJAFQ26sYh7MOKdU+VQFgPg==
|
||||
|
||||
"@next/swc-win32-arm64-msvc@14.2.15":
|
||||
version "14.2.15"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.15.tgz#fb812cc4ca0042868e32a6a021da91943bb08b98"
|
||||
integrity sha512-2raR16703kBvYEQD9HNLyb0/394yfqzmIeyp2nDzcPV4yPjqNUG3ohX6jX00WryXz6s1FXpVhsCo3i+g4RUX+g==
|
||||
"@next/swc-win32-arm64-msvc@14.2.21":
|
||||
version "14.2.21"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.21.tgz#290012ee57b196d3d2d04853e6bf0179cae9fbaf"
|
||||
integrity sha512-plykgB3vL2hB4Z32W3ktsfqyuyGAPxqwiyrAi2Mr8LlEUhNn9VgkiAl5hODSBpzIfWweX3er1f5uNpGDygfQVQ==
|
||||
|
||||
"@next/swc-win32-ia32-msvc@14.2.15":
|
||||
version "14.2.15"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.15.tgz#ec26e6169354f8ced240c1427be7fd485c5df898"
|
||||
integrity sha512-fyTE8cklgkyR1p03kJa5zXEaZ9El+kDNM5A+66+8evQS5e/6v0Gk28LqA0Jet8gKSOyP+OTm/tJHzMlGdQerdQ==
|
||||
"@next/swc-win32-ia32-msvc@14.2.21":
|
||||
version "14.2.21"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.21.tgz#c959135a78cab18cca588d11d1e33bcf199590d4"
|
||||
integrity sha512-w5bacz4Vxqrh06BjWgua3Yf7EMDb8iMcVhNrNx8KnJXt8t+Uu0Zg4JHLDL/T7DkTCEEfKXO/Er1fcfWxn2xfPA==
|
||||
|
||||
"@next/swc-win32-x64-msvc@14.2.15":
|
||||
version "14.2.15"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.15.tgz#18d68697002b282006771f8d92d79ade9efd35c4"
|
||||
integrity sha512-SzqGbsLsP9OwKNUG9nekShTwhj6JSB9ZLMWQ8g1gG6hdE5gQLncbnbymrwy2yVmH9nikSLYRYxYMFu78Ggp7/g==
|
||||
"@next/swc-win32-x64-msvc@14.2.21":
|
||||
version "14.2.21"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.21.tgz#21ff892286555b90538a7d1b505ea21a005d6ead"
|
||||
integrity sha512-sT6+llIkzpsexGYZq8cjjthRyRGe5cJVhqh12FmlbxHqna6zsDDK8UNaV7g41T6atFHCJUPeLb3uyAwrBwy0NA==
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
|
@ -1253,12 +1253,12 @@ natural-compare@^1.4.0:
|
|||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
|
||||
|
||||
next@14.2.15:
|
||||
version "14.2.15"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-14.2.15.tgz#348e5603e22649775d19c785c09a89c9acb5189a"
|
||||
integrity sha512-h9ctmOokpoDphRvMGnwOJAedT6zKhwqyZML9mDtspgf4Rh3Pn7UTYKqePNoDvhsWBAO5GoPNYshnAUGIazVGmw==
|
||||
next@14.2.21:
|
||||
version "14.2.21"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-14.2.21.tgz#f6da9e2abba1a0e4ca7a5273825daf06632554ba"
|
||||
integrity sha512-rZmLwucLHr3/zfDMYbJXbw0ZeoBpirxkXuvsJbk7UPorvPYZhP7vq7aHbKnU7dQNCYIimRrbB2pp3xmf+wsYUg==
|
||||
dependencies:
|
||||
"@next/env" "14.2.15"
|
||||
"@next/env" "14.2.21"
|
||||
"@swc/helpers" "0.5.5"
|
||||
busboy "1.6.0"
|
||||
caniuse-lite "^1.0.30001579"
|
||||
|
@ -1266,15 +1266,15 @@ next@14.2.15:
|
|||
postcss "8.4.31"
|
||||
styled-jsx "5.1.1"
|
||||
optionalDependencies:
|
||||
"@next/swc-darwin-arm64" "14.2.15"
|
||||
"@next/swc-darwin-x64" "14.2.15"
|
||||
"@next/swc-linux-arm64-gnu" "14.2.15"
|
||||
"@next/swc-linux-arm64-musl" "14.2.15"
|
||||
"@next/swc-linux-x64-gnu" "14.2.15"
|
||||
"@next/swc-linux-x64-musl" "14.2.15"
|
||||
"@next/swc-win32-arm64-msvc" "14.2.15"
|
||||
"@next/swc-win32-ia32-msvc" "14.2.15"
|
||||
"@next/swc-win32-x64-msvc" "14.2.15"
|
||||
"@next/swc-darwin-arm64" "14.2.21"
|
||||
"@next/swc-darwin-x64" "14.2.21"
|
||||
"@next/swc-linux-arm64-gnu" "14.2.21"
|
||||
"@next/swc-linux-arm64-musl" "14.2.21"
|
||||
"@next/swc-linux-x64-gnu" "14.2.21"
|
||||
"@next/swc-linux-x64-musl" "14.2.21"
|
||||
"@next/swc-win32-arm64-msvc" "14.2.21"
|
||||
"@next/swc-win32-ia32-msvc" "14.2.21"
|
||||
"@next/swc-win32-x64-msvc" "14.2.21"
|
||||
|
||||
node-domexception@^1.0.0:
|
||||
version "1.0.0"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||
"version": "3.2.29",
|
||||
"version": "3.2.32",
|
||||
"npmClient": "yarn",
|
||||
"concurrency": 20,
|
||||
"command": {
|
||||
|
|
|
@ -385,17 +385,17 @@ export function getCurrentContext(): ContextMap | undefined {
|
|||
}
|
||||
}
|
||||
|
||||
export function getFeatureFlags<T extends Record<string, any>>(
|
||||
export function getFeatureFlags(
|
||||
key: string
|
||||
): T | undefined {
|
||||
): Record<string, boolean> | undefined {
|
||||
const context = getCurrentContext()
|
||||
if (!context) {
|
||||
return undefined
|
||||
}
|
||||
return context.featureFlagCache?.[key] as T
|
||||
return context.featureFlagCache?.[key]
|
||||
}
|
||||
|
||||
export function setFeatureFlags(key: string, value: Record<string, any>) {
|
||||
export function setFeatureFlags(key: string, value: Record<string, boolean>) {
|
||||
const context = getCurrentContext()
|
||||
if (!context) {
|
||||
return
|
||||
|
|
|
@ -20,7 +20,7 @@ export type ContextMap = {
|
|||
clients: Record<string, GoogleSpreadsheet>
|
||||
}
|
||||
featureFlagCache?: {
|
||||
[key: string]: Record<string, any>
|
||||
[key: string]: Record<string, boolean>
|
||||
}
|
||||
viewToTableCache?: Record<string, Table>
|
||||
}
|
||||
|
|
|
@ -2,9 +2,10 @@ import env from "../environment"
|
|||
import * as crypto from "crypto"
|
||||
import * as context from "../context"
|
||||
import { PostHog, PostHogOptions } from "posthog-node"
|
||||
import { FeatureFlag } from "@budibase/types"
|
||||
import tracer from "dd-trace"
|
||||
import { Duration } from "../utils"
|
||||
import { cloneDeep } from "lodash"
|
||||
import { FeatureFlagDefaults } from "@budibase/types"
|
||||
|
||||
let posthog: PostHog | undefined
|
||||
export function init(opts?: PostHogOptions) {
|
||||
|
@ -30,74 +31,6 @@ export function shutdown() {
|
|||
posthog?.shutdown()
|
||||
}
|
||||
|
||||
export abstract class Flag<T> {
|
||||
static boolean(defaultValue: boolean): Flag<boolean> {
|
||||
return new BooleanFlag(defaultValue)
|
||||
}
|
||||
|
||||
static string(defaultValue: string): Flag<string> {
|
||||
return new StringFlag(defaultValue)
|
||||
}
|
||||
|
||||
static number(defaultValue: number): Flag<number> {
|
||||
return new NumberFlag(defaultValue)
|
||||
}
|
||||
|
||||
protected constructor(public defaultValue: T) {}
|
||||
|
||||
abstract parse(value: any): T
|
||||
}
|
||||
|
||||
type UnwrapFlag<F> = F extends Flag<infer U> ? U : never
|
||||
|
||||
export type FlagValues<T> = {
|
||||
[K in keyof T]: UnwrapFlag<T[K]>
|
||||
}
|
||||
|
||||
type KeysOfType<T, U> = {
|
||||
[K in keyof T]: T[K] extends Flag<U> ? K : never
|
||||
}[keyof T]
|
||||
|
||||
class BooleanFlag extends Flag<boolean> {
|
||||
parse(value: any) {
|
||||
if (typeof value === "string") {
|
||||
return ["true", "t", "1"].includes(value.toLowerCase())
|
||||
}
|
||||
|
||||
if (typeof value === "boolean") {
|
||||
return value
|
||||
}
|
||||
|
||||
throw new Error(`could not parse value "${value}" as boolean`)
|
||||
}
|
||||
}
|
||||
|
||||
class StringFlag extends Flag<string> {
|
||||
parse(value: any) {
|
||||
if (typeof value === "string") {
|
||||
return value
|
||||
}
|
||||
throw new Error(`could not parse value "${value}" as string`)
|
||||
}
|
||||
}
|
||||
|
||||
class NumberFlag extends Flag<number> {
|
||||
parse(value: any) {
|
||||
if (typeof value === "number") {
|
||||
return value
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
const parsed = parseFloat(value)
|
||||
if (!isNaN(parsed)) {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`could not parse value "${value}" as number`)
|
||||
}
|
||||
}
|
||||
|
||||
export interface EnvFlagEntry {
|
||||
tenantId: string
|
||||
key: string
|
||||
|
@ -120,7 +53,7 @@ export function parseEnvFlags(flags: string): EnvFlagEntry[] {
|
|||
return result
|
||||
}
|
||||
|
||||
export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
||||
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
|
||||
// one to leak into another.
|
||||
|
@ -130,34 +63,25 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
|||
this.setId = crypto.randomUUID()
|
||||
}
|
||||
|
||||
defaults(): FlagValues<T> {
|
||||
return Object.keys(this.flagSchema).reduce((acc, key) => {
|
||||
const typedKey = key as keyof T
|
||||
acc[typedKey] = this.flagSchema[key].defaultValue
|
||||
return acc
|
||||
}, {} as FlagValues<T>)
|
||||
defaults(): T {
|
||||
return cloneDeep(this.flagSchema)
|
||||
}
|
||||
|
||||
isFlagName(name: string | number | symbol): name is keyof T {
|
||||
return this.flagSchema[name as keyof T] !== undefined
|
||||
}
|
||||
|
||||
async get<K extends keyof T>(key: K): Promise<FlagValues<T>[K]> {
|
||||
async isEnabled<K extends keyof T>(key: K): Promise<T[K]> {
|
||||
const flags = await this.fetch()
|
||||
return flags[key]
|
||||
}
|
||||
|
||||
async isEnabled<K extends KeysOfType<T, boolean>>(key: K): Promise<boolean> {
|
||||
const flags = await this.fetch()
|
||||
return flags[key]
|
||||
}
|
||||
|
||||
async fetch(): Promise<FlagValues<T>> {
|
||||
async fetch(): Promise<T> {
|
||||
return await tracer.trace("features.fetch", async span => {
|
||||
const cachedFlags = context.getFeatureFlags<FlagValues<T>>(this.setId)
|
||||
const cachedFlags = context.getFeatureFlags(this.setId)
|
||||
if (cachedFlags) {
|
||||
span?.addTags({ fromCache: true })
|
||||
return cachedFlags
|
||||
return cachedFlags as T
|
||||
}
|
||||
|
||||
const tags: Record<string, any> = {}
|
||||
|
@ -189,7 +113,7 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
|||
|
||||
// @ts-expect-error - TS does not like you writing into a generic type,
|
||||
// but we know that it's okay in this case because it's just an object.
|
||||
flagValues[key as keyof FlagValues] = value
|
||||
flagValues[key as keyof T] = value
|
||||
tags[`flags.${key}.source`] = "environment"
|
||||
}
|
||||
|
||||
|
@ -217,11 +141,11 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
|||
tags[`readFromPostHog`] = true
|
||||
|
||||
const personProperties: Record<string, string> = { tenantId }
|
||||
const posthogFlags = await posthog.getAllFlagsAndPayloads(userId, {
|
||||
const posthogFlags = await posthog.getAllFlags(userId, {
|
||||
personProperties,
|
||||
})
|
||||
|
||||
for (const [name, value] of Object.entries(posthogFlags.featureFlags)) {
|
||||
for (const [name, value] of Object.entries(posthogFlags)) {
|
||||
if (!this.isFlagName(name)) {
|
||||
// We don't want an unexpected PostHog flag to break the app, so we
|
||||
// just log it and continue.
|
||||
|
@ -229,19 +153,20 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
|||
continue
|
||||
}
|
||||
|
||||
if (typeof value !== "boolean") {
|
||||
console.warn(`Invalid value for posthog flag "${name}": ${value}`)
|
||||
continue
|
||||
}
|
||||
|
||||
if (flagValues[name] === true || specificallySetFalse.has(name)) {
|
||||
// If the flag is already set to through environment variables, we
|
||||
// don't want to override it back to false here.
|
||||
continue
|
||||
}
|
||||
|
||||
const payload = posthogFlags.featureFlagPayloads?.[name]
|
||||
const flag = this.flagSchema[name]
|
||||
try {
|
||||
// @ts-expect-error - TS does not like you writing into a generic
|
||||
// type, but we know that it's okay in this case because it's just
|
||||
// an object.
|
||||
flagValues[name] = flag.parse(payload || value)
|
||||
// @ts-expect-error - TS does not like you writing into a generic type.
|
||||
flagValues[name] = value
|
||||
tags[`flags.${name}.source`] = "posthog"
|
||||
} catch (err) {
|
||||
// We don't want an invalid PostHog flag to break the app, so we just
|
||||
|
@ -262,18 +187,12 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
|||
}
|
||||
}
|
||||
|
||||
// This is the primary source of truth for feature flags. If you want to add a
|
||||
// new flag, add it here and use the `fetch` and `get` functions to access it.
|
||||
// All of the machinery in this file is to make sure that flags have their
|
||||
// default values set correctly and their types flow through the system.
|
||||
const flagsConfig: Record<FeatureFlag, Flag<any>> = {
|
||||
[FeatureFlag.DEFAULT_VALUES]: Flag.boolean(true),
|
||||
[FeatureFlag.AUTOMATION_BRANCHING]: Flag.boolean(true),
|
||||
[FeatureFlag.AI_CUSTOM_CONFIGS]: Flag.boolean(true),
|
||||
[FeatureFlag.BUDIBASE_AI]: Flag.boolean(true),
|
||||
[FeatureFlag.USE_ZOD_VALIDATOR]: Flag.boolean(env.isDev()),
|
||||
}
|
||||
export const flags = new FlagSet(flagsConfig)
|
||||
export const flags = new FlagSet(FeatureFlagDefaults)
|
||||
|
||||
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T
|
||||
export type FeatureFlags = UnwrapPromise<ReturnType<typeof flags.fetch>>
|
||||
export async function isEnabled(flag: keyof typeof FeatureFlagDefaults) {
|
||||
return await flags.isEnabled(flag)
|
||||
}
|
||||
|
||||
export async function all() {
|
||||
return await flags.fetch()
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { IdentityContext, IdentityType } from "@budibase/types"
|
||||
import { Flag, FlagSet, FlagValues, init, shutdown } from "../"
|
||||
import { FlagSet, init, shutdown } from "../"
|
||||
import * as context from "../../context"
|
||||
import environment, { withEnv } from "../../environment"
|
||||
import nodeFetch from "node-fetch"
|
||||
|
@ -7,10 +7,8 @@ import nock from "nock"
|
|||
import * as crypto from "crypto"
|
||||
|
||||
const schema = {
|
||||
TEST_BOOLEAN: Flag.boolean(false),
|
||||
TEST_STRING: Flag.string("default value"),
|
||||
TEST_NUMBER: Flag.number(0),
|
||||
TEST_BOOLEAN_DEFAULT_TRUE: Flag.boolean(true),
|
||||
TEST_BOOLEAN: false,
|
||||
TEST_BOOLEAN_DEFAULT_TRUE: true,
|
||||
}
|
||||
const flags = new FlagSet(schema)
|
||||
|
||||
|
@ -19,7 +17,7 @@ interface TestCase {
|
|||
identity?: Partial<IdentityContext>
|
||||
environmentFlags?: string
|
||||
posthogFlags?: PostHogFlags
|
||||
expected?: Partial<FlagValues<typeof schema>>
|
||||
expected?: Partial<typeof schema>
|
||||
errorMessage?: string | RegExp
|
||||
}
|
||||
|
||||
|
@ -83,22 +81,6 @@ describe("feature flags", () => {
|
|||
},
|
||||
expected: { TEST_BOOLEAN: true },
|
||||
},
|
||||
{
|
||||
it: "should be able to read string flags from PostHog",
|
||||
posthogFlags: {
|
||||
featureFlags: { TEST_STRING: true },
|
||||
featureFlagPayloads: { TEST_STRING: "test" },
|
||||
},
|
||||
expected: { TEST_STRING: "test" },
|
||||
},
|
||||
{
|
||||
it: "should be able to read numeric flags from PostHog",
|
||||
posthogFlags: {
|
||||
featureFlags: { TEST_NUMBER: true },
|
||||
featureFlagPayloads: { TEST_NUMBER: "123" },
|
||||
},
|
||||
expected: { TEST_NUMBER: 123 },
|
||||
},
|
||||
{
|
||||
it: "should not be able to override a negative environment flag from PostHog",
|
||||
environmentFlags: "default:!TEST_BOOLEAN",
|
||||
|
@ -177,7 +159,7 @@ describe("feature flags", () => {
|
|||
expect(values).toMatchObject(expected)
|
||||
|
||||
for (const [key, expectedValue] of Object.entries(expected)) {
|
||||
const value = await flags.get(key as keyof typeof schema)
|
||||
const value = await flags.isEnabled(key as keyof typeof schema)
|
||||
expect(value).toBe(expectedValue)
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { FeatureFlags, parseEnvFlags } from ".."
|
||||
import { FeatureFlags } from "@budibase/types"
|
||||
import { setEnv } from "../../environment"
|
||||
import { parseEnvFlags } from "../features"
|
||||
|
||||
function getCurrentFlags(): Record<string, Record<string, boolean>> {
|
||||
const result: Record<string, Record<string, boolean>> = {}
|
||||
|
|
|
@ -291,8 +291,8 @@ const automationActions = (store: AutomationStore) => ({
|
|||
let result: (AutomationStep | AutomationTrigger)[] = []
|
||||
pathWay.forEach(path => {
|
||||
const { stepIdx, branchIdx } = path
|
||||
let last = result ? result[result.length - 1] : []
|
||||
if (!result) {
|
||||
let last = result.length ? result[result.length - 1] : []
|
||||
if (!result.length) {
|
||||
// Preceeding steps.
|
||||
result = steps.slice(0, stepIdx + 1)
|
||||
return
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
import { writable } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
|
||||
export function createFlagsStore() {
|
||||
const { subscribe, set } = writable({})
|
||||
|
||||
const actions = {
|
||||
fetch: async () => {
|
||||
const flags = await API.getFlags()
|
||||
set(flags)
|
||||
},
|
||||
updateFlag: async (flag, value) => {
|
||||
await API.updateFlag(flag, value)
|
||||
await actions.fetch()
|
||||
},
|
||||
toggleUiFeature: async feature => {
|
||||
await API.toggleUiFeature(feature)
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
...actions,
|
||||
}
|
||||
}
|
||||
|
||||
export const flags = createFlagsStore()
|
|
@ -0,0 +1,40 @@
|
|||
import { API } from "@/api"
|
||||
import { GetUserFlagsResponse } from "@budibase/types"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
|
||||
interface FlagsState {
|
||||
flags: GetUserFlagsResponse
|
||||
}
|
||||
|
||||
const INITIAL_FLAGS_STATE: FlagsState = {
|
||||
flags: {},
|
||||
}
|
||||
|
||||
export class FlagsStore extends BudiStore<FlagsState> {
|
||||
constructor() {
|
||||
super(INITIAL_FLAGS_STATE)
|
||||
|
||||
this.fetch = this.fetch.bind(this)
|
||||
this.updateFlag = this.updateFlag.bind(this)
|
||||
this.toggleUiFeature = this.toggleUiFeature.bind(this)
|
||||
}
|
||||
|
||||
async fetch() {
|
||||
const flags = await API.getFlags()
|
||||
this.update(state => ({
|
||||
...state,
|
||||
flags,
|
||||
}))
|
||||
}
|
||||
|
||||
async updateFlag(flag: string, value: any) {
|
||||
await API.updateFlag(flag, value)
|
||||
await this.fetch()
|
||||
}
|
||||
|
||||
async toggleUiFeature(feature: string) {
|
||||
await API.toggleUiFeature(feature)
|
||||
}
|
||||
}
|
||||
|
||||
export const flags = new FlagsStore()
|
|
@ -2,19 +2,24 @@ import { get } from "svelte/store"
|
|||
import { BudiStore } from "../BudiStore"
|
||||
import { previewStore } from "@/stores/builder"
|
||||
|
||||
interface BuilderHoverStore {
|
||||
hoverTimeout?: NodeJS.Timeout
|
||||
componentId: string | null
|
||||
}
|
||||
|
||||
export const INITIAL_HOVER_STATE = {
|
||||
componentId: null,
|
||||
}
|
||||
|
||||
export class HoverStore extends BudiStore {
|
||||
hoverTimeout
|
||||
export class HoverStore extends BudiStore<BuilderHoverStore> {
|
||||
hoverTimeout?: NodeJS.Timeout
|
||||
|
||||
constructor() {
|
||||
super({ ...INITIAL_HOVER_STATE })
|
||||
this.hover = this.hover.bind(this)
|
||||
}
|
||||
|
||||
hover(componentId, notifyClient = true) {
|
||||
hover(componentId: string, notifyClient = true) {
|
||||
clearTimeout(this.hoverTimeout)
|
||||
if (componentId) {
|
||||
this.processHover(componentId, notifyClient)
|
||||
|
@ -25,7 +30,7 @@ export class HoverStore extends BudiStore {
|
|||
}
|
||||
}
|
||||
|
||||
processHover(componentId, notifyClient) {
|
||||
processHover(componentId: string, notifyClient?: boolean) {
|
||||
if (componentId === get(this.store).componentId) {
|
||||
return
|
||||
}
|
|
@ -2,13 +2,19 @@ import { derived, get } from "svelte/store"
|
|||
import { componentStore } from "@/stores/builder"
|
||||
import { API } from "@/api"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
import { Layout } from "@budibase/types"
|
||||
|
||||
export const INITIAL_LAYOUT_STATE = {
|
||||
interface LayoutState {
|
||||
layouts: Layout[]
|
||||
selectedLayoutId: string | null
|
||||
}
|
||||
|
||||
export const INITIAL_LAYOUT_STATE: LayoutState = {
|
||||
layouts: [],
|
||||
selectedLayoutId: null,
|
||||
}
|
||||
|
||||
export class LayoutStore extends BudiStore {
|
||||
export class LayoutStore extends BudiStore<LayoutState> {
|
||||
constructor() {
|
||||
super(INITIAL_LAYOUT_STATE)
|
||||
|
||||
|
@ -22,14 +28,14 @@ export class LayoutStore extends BudiStore {
|
|||
this.store.set({ ...INITIAL_LAYOUT_STATE })
|
||||
}
|
||||
|
||||
syncAppLayouts(pkg) {
|
||||
syncAppLayouts(pkg: { layouts: Layout[] }) {
|
||||
this.update(state => ({
|
||||
...state,
|
||||
layouts: [...pkg.layouts],
|
||||
}))
|
||||
}
|
||||
|
||||
select(layoutId) {
|
||||
select(layoutId: string) {
|
||||
// Check this layout exists
|
||||
const state = get(this.store)
|
||||
const componentState = get(componentStore)
|
||||
|
@ -48,15 +54,15 @@ export class LayoutStore extends BudiStore {
|
|||
|
||||
// Select new layout
|
||||
this.update(state => {
|
||||
state.selectedLayoutId = layout._id
|
||||
state.selectedLayoutId = layout._id!
|
||||
return state
|
||||
})
|
||||
|
||||
componentStore.select(layout.props?._id)
|
||||
}
|
||||
|
||||
async deleteLayout(layout) {
|
||||
if (!layout?._id) {
|
||||
async deleteLayout(layout: Layout) {
|
||||
if (!layout?._id || !layout?._rev) {
|
||||
return
|
||||
}
|
||||
await API.deleteLayout(layout._id, layout._rev)
|
|
@ -2,27 +2,22 @@ import { get } from "svelte/store"
|
|||
import { API } from "@/api"
|
||||
import { appStore } from "@/stores/builder"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
import { AppNavigation, AppNavigationLink, UIObject } from "@budibase/types"
|
||||
|
||||
interface BuilderNavigationStore extends AppNavigation {}
|
||||
|
||||
export const INITIAL_NAVIGATION_STATE = {
|
||||
navigation: "Top",
|
||||
links: [],
|
||||
title: null,
|
||||
sticky: null,
|
||||
hideLogo: null,
|
||||
logoUrl: null,
|
||||
hideTitle: null,
|
||||
textAlign: "Left",
|
||||
navBackground: null,
|
||||
navWidth: null,
|
||||
navTextColor: null,
|
||||
}
|
||||
|
||||
export class NavigationStore extends BudiStore {
|
||||
export class NavigationStore extends BudiStore<BuilderNavigationStore> {
|
||||
constructor() {
|
||||
super(INITIAL_NAVIGATION_STATE)
|
||||
}
|
||||
|
||||
syncAppNavigation(nav) {
|
||||
syncAppNavigation(nav: AppNavigation) {
|
||||
this.update(state => ({
|
||||
...state,
|
||||
...nav,
|
||||
|
@ -33,15 +28,17 @@ export class NavigationStore extends BudiStore {
|
|||
this.store.set({ ...INITIAL_NAVIGATION_STATE })
|
||||
}
|
||||
|
||||
async save(navigation) {
|
||||
async save(navigation: AppNavigation) {
|
||||
const appId = get(appStore).appId
|
||||
const app = await API.saveAppMetadata(appId, { navigation })
|
||||
this.syncAppNavigation(app.navigation)
|
||||
if (app.navigation) {
|
||||
this.syncAppNavigation(app.navigation)
|
||||
}
|
||||
}
|
||||
|
||||
async saveLink(url, title, roleId) {
|
||||
async saveLink(url: string, title: string, roleId: string) {
|
||||
const navigation = get(this.store)
|
||||
let links = [...(navigation?.links ?? [])]
|
||||
let links: AppNavigationLink[] = [...(navigation?.links ?? [])]
|
||||
|
||||
// Skip if we have an identical link
|
||||
if (links.find(link => link.url === url && link.text === title)) {
|
||||
|
@ -60,7 +57,7 @@ export class NavigationStore extends BudiStore {
|
|||
})
|
||||
}
|
||||
|
||||
async deleteLink(urls) {
|
||||
async deleteLink(urls: string[] | string) {
|
||||
const navigation = get(this.store)
|
||||
let links = navigation?.links
|
||||
if (!links?.length) {
|
||||
|
@ -86,7 +83,7 @@ export class NavigationStore extends BudiStore {
|
|||
})
|
||||
}
|
||||
|
||||
syncMetadata(metadata) {
|
||||
syncMetadata(metadata: UIObject) {
|
||||
const { navigation } = metadata
|
||||
this.syncAppNavigation(navigation)
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
|
||||
const INITIAL_PREVIEW_STATE = {
|
||||
previewDevice: "desktop",
|
||||
previewEventHandler: null,
|
||||
showPreview: false,
|
||||
selectedComponentContext: null,
|
||||
}
|
||||
|
||||
export const createPreviewStore = () => {
|
||||
const store = writable({
|
||||
...INITIAL_PREVIEW_STATE,
|
||||
})
|
||||
|
||||
const setDevice = device => {
|
||||
store.update(state => {
|
||||
state.previewDevice = device
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
// Potential evt names "eject-block", "dragging-new-component"
|
||||
const sendEvent = (name, payload) => {
|
||||
const { previewEventHandler } = get(store)
|
||||
previewEventHandler?.(name, payload)
|
||||
}
|
||||
|
||||
const registerEventHandler = handler => {
|
||||
store.update(state => {
|
||||
state.previewEventHandler = handler
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
const startDrag = component => {
|
||||
sendEvent("dragging-new-component", {
|
||||
dragging: true,
|
||||
component,
|
||||
})
|
||||
}
|
||||
|
||||
const stopDrag = () => {
|
||||
sendEvent("dragging-new-component", {
|
||||
dragging: false,
|
||||
})
|
||||
}
|
||||
|
||||
//load preview?
|
||||
const showPreview = isVisible => {
|
||||
store.update(state => {
|
||||
state.showPreview = isVisible
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
const setSelectedComponentContext = context => {
|
||||
store.update(state => {
|
||||
state.selectedComponentContext = context
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
const requestComponentContext = () => {
|
||||
sendEvent("request-context")
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
setDevice,
|
||||
sendEvent, //may not be required
|
||||
registerEventHandler,
|
||||
startDrag,
|
||||
stopDrag,
|
||||
showPreview,
|
||||
setSelectedComponentContext,
|
||||
requestComponentContext,
|
||||
}
|
||||
}
|
||||
|
||||
export const previewStore = createPreviewStore()
|
|
@ -0,0 +1,90 @@
|
|||
import { get } from "svelte/store"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
|
||||
type PreviewDevice = "desktop" | "tablet" | "mobile"
|
||||
type PreviewEventHandler = (name: string, payload?: any) => void
|
||||
type ComponentContext = Record<string, any>
|
||||
|
||||
interface PreviewState {
|
||||
previewDevice: PreviewDevice
|
||||
previewEventHandler: PreviewEventHandler | null
|
||||
showPreview: boolean
|
||||
selectedComponentContext: ComponentContext | null
|
||||
}
|
||||
|
||||
const INITIAL_PREVIEW_STATE: PreviewState = {
|
||||
previewDevice: "desktop",
|
||||
previewEventHandler: null,
|
||||
showPreview: false,
|
||||
selectedComponentContext: null,
|
||||
}
|
||||
|
||||
export class PreviewStore extends BudiStore<PreviewState> {
|
||||
constructor() {
|
||||
super(INITIAL_PREVIEW_STATE)
|
||||
|
||||
this.setDevice = this.setDevice.bind(this)
|
||||
this.sendEvent = this.sendEvent.bind(this)
|
||||
this.registerEventHandler = this.registerEventHandler.bind(this)
|
||||
this.startDrag = this.startDrag.bind(this)
|
||||
this.stopDrag = this.stopDrag.bind(this)
|
||||
this.showPreview = this.showPreview.bind(this)
|
||||
this.setSelectedComponentContext =
|
||||
this.setSelectedComponentContext.bind(this)
|
||||
this.requestComponentContext = this.requestComponentContext.bind(this)
|
||||
}
|
||||
|
||||
setDevice(device: PreviewDevice) {
|
||||
this.update(state => ({
|
||||
...state,
|
||||
previewDevice: device,
|
||||
}))
|
||||
}
|
||||
|
||||
// Potential evt names "eject-block", "dragging-new-component"
|
||||
sendEvent(name: string, payload?: any) {
|
||||
const { previewEventHandler } = get(this.store)
|
||||
previewEventHandler?.(name, payload)
|
||||
}
|
||||
|
||||
registerEventHandler(handler: PreviewEventHandler) {
|
||||
this.update(state => ({
|
||||
...state,
|
||||
previewEventHandler: handler,
|
||||
}))
|
||||
}
|
||||
|
||||
startDrag(component: any) {
|
||||
this.sendEvent("dragging-new-component", {
|
||||
dragging: true,
|
||||
component,
|
||||
})
|
||||
}
|
||||
|
||||
stopDrag() {
|
||||
this.sendEvent("dragging-new-component", {
|
||||
dragging: false,
|
||||
})
|
||||
}
|
||||
|
||||
//load preview?
|
||||
showPreview(isVisible: boolean) {
|
||||
this.update(state => ({
|
||||
...state,
|
||||
showPreview: isVisible,
|
||||
}))
|
||||
}
|
||||
|
||||
setSelectedComponentContext(context: ComponentContext) {
|
||||
this.update(state => ({
|
||||
...state,
|
||||
selectedComponentContext: context,
|
||||
}))
|
||||
}
|
||||
|
||||
requestComponentContext() {
|
||||
this.sendEvent("request-context")
|
||||
}
|
||||
}
|
||||
|
||||
export const previewStore = new PreviewStore()
|
|
@ -6,18 +6,29 @@ import { automationStore } from "./automations"
|
|||
import { API } from "@/api"
|
||||
import { getSequentialName } from "@/helpers/duplicate"
|
||||
|
||||
const initialState = {}
|
||||
interface RowAction {
|
||||
id: string
|
||||
name: string
|
||||
tableId: string
|
||||
allowedSources?: string[]
|
||||
}
|
||||
|
||||
export class RowActionStore extends BudiStore {
|
||||
interface RowActionState {
|
||||
[tableId: string]: RowAction[]
|
||||
}
|
||||
|
||||
const initialState: RowActionState = {}
|
||||
|
||||
export class RowActionStore extends BudiStore<RowActionState> {
|
||||
constructor() {
|
||||
super(initialState)
|
||||
}
|
||||
|
||||
reset = () => {
|
||||
this.store.set(initialState)
|
||||
this.set(initialState)
|
||||
}
|
||||
|
||||
refreshRowActions = async sourceId => {
|
||||
refreshRowActions = async (sourceId: string) => {
|
||||
if (!sourceId) {
|
||||
return
|
||||
}
|
||||
|
@ -34,26 +45,30 @@ export class RowActionStore extends BudiStore {
|
|||
|
||||
// Fetch row actions for this table
|
||||
const res = await API.rowActions.fetch(tableId)
|
||||
const actions = Object.values(res || {})
|
||||
const actions = Object.values(res || {}) as RowAction[]
|
||||
this.update(state => ({
|
||||
...state,
|
||||
[tableId]: actions,
|
||||
}))
|
||||
}
|
||||
|
||||
createRowAction = async (tableId, viewId, name) => {
|
||||
createRowAction = async (tableId: string, viewId?: string, name?: string) => {
|
||||
if (!tableId) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get a unique name for this action
|
||||
if (!name) {
|
||||
const existingRowActions = get(this.store)[tableId] || []
|
||||
const existingRowActions = get(this)[tableId] || []
|
||||
name = getSequentialName(existingRowActions, "New row action ", {
|
||||
getName: x => x.name,
|
||||
})
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
throw new Error("Failed to generate a unique name for the row action")
|
||||
}
|
||||
|
||||
// Create the action
|
||||
const res = await API.rowActions.create(tableId, name)
|
||||
|
||||
|
@ -73,41 +88,35 @@ export class RowActionStore extends BudiStore {
|
|||
return res
|
||||
}
|
||||
|
||||
enableView = async (tableId, rowActionId, viewId) => {
|
||||
enableView = async (tableId: string, rowActionId: string, viewId: string) => {
|
||||
await API.rowActions.enableView(tableId, rowActionId, viewId)
|
||||
await this.refreshRowActions(tableId)
|
||||
}
|
||||
|
||||
disableView = async (tableId, rowActionId, viewId) => {
|
||||
disableView = async (
|
||||
tableId: string,
|
||||
rowActionId: string,
|
||||
viewId: string
|
||||
) => {
|
||||
await API.rowActions.disableView(tableId, rowActionId, viewId)
|
||||
await this.refreshRowActions(tableId)
|
||||
}
|
||||
|
||||
rename = async (tableId, rowActionId, name) => {
|
||||
await API.rowActions.update({
|
||||
tableId,
|
||||
rowActionId,
|
||||
name,
|
||||
})
|
||||
await this.refreshRowActions(tableId)
|
||||
automationStore.actions.fetch()
|
||||
}
|
||||
|
||||
delete = async (tableId, rowActionId) => {
|
||||
delete = async (tableId: string, rowActionId: string) => {
|
||||
await API.rowActions.delete(tableId, rowActionId)
|
||||
await this.refreshRowActions(tableId)
|
||||
// We don't need to refresh automations as we can only delete row actions
|
||||
// from the automations store, so we already handle the state update there
|
||||
}
|
||||
|
||||
trigger = async (sourceId, rowActionId, rowId) => {
|
||||
trigger = async (sourceId: string, rowActionId: string, rowId: string) => {
|
||||
await API.rowActions.trigger(sourceId, rowActionId, rowId)
|
||||
}
|
||||
}
|
||||
|
||||
const store = new RowActionStore()
|
||||
const derivedStore = derived(store, $store => {
|
||||
let map = {}
|
||||
const derivedStore = derived<RowActionStore, RowActionState>(store, $store => {
|
||||
const map: RowActionState = {}
|
||||
|
||||
// Generate an entry for every view as well
|
||||
Object.keys($store || {}).forEach(tableId => {
|
||||
|
@ -115,7 +124,7 @@ const derivedStore = derived(store, $store => {
|
|||
map[tableId] = $store[tableId]
|
||||
for (let action of $store[tableId]) {
|
||||
const otherSources = (action.allowedSources || []).filter(
|
||||
sourceId => sourceId !== tableId
|
||||
(sourceId: string) => sourceId !== tableId
|
||||
)
|
||||
for (let source of otherSources) {
|
||||
map[source] ??= []
|
|
@ -1,35 +0,0 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
import { appStore } from "./app"
|
||||
|
||||
const createsnippets = () => {
|
||||
const store = writable([])
|
||||
|
||||
const syncMetadata = metadata => {
|
||||
store.set(metadata?.snippets || [])
|
||||
}
|
||||
|
||||
const saveSnippet = async updatedSnippet => {
|
||||
const snippets = [
|
||||
...get(store).filter(snippet => snippet.name !== updatedSnippet.name),
|
||||
updatedSnippet,
|
||||
]
|
||||
const app = await API.saveAppMetadata(get(appStore).appId, { snippets })
|
||||
syncMetadata(app)
|
||||
}
|
||||
|
||||
const deleteSnippet = async snippetName => {
|
||||
const snippets = get(store).filter(snippet => snippet.name !== snippetName)
|
||||
const app = await API.saveAppMetadata(get(appStore).appId, { snippets })
|
||||
syncMetadata(app)
|
||||
}
|
||||
|
||||
return {
|
||||
...store,
|
||||
syncMetadata,
|
||||
saveSnippet,
|
||||
deleteSnippet,
|
||||
}
|
||||
}
|
||||
|
||||
export const snippets = createsnippets()
|
|
@ -0,0 +1,32 @@
|
|||
import { get } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
import { appStore } from "./app"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
import { Snippet, UpdateAppResponse } from "@budibase/types"
|
||||
|
||||
export class SnippetStore extends BudiStore<Snippet[]> {
|
||||
constructor() {
|
||||
super([])
|
||||
}
|
||||
|
||||
syncMetadata = (metadata: UpdateAppResponse) => {
|
||||
this.set(metadata?.snippets || [])
|
||||
}
|
||||
|
||||
saveSnippet = async (updatedSnippet: Snippet) => {
|
||||
const snippets = [
|
||||
...get(this).filter(snippet => snippet.name !== updatedSnippet.name),
|
||||
updatedSnippet,
|
||||
]
|
||||
const app = await API.saveAppMetadata(get(appStore).appId, { snippets })
|
||||
this.syncMetadata(app)
|
||||
}
|
||||
|
||||
deleteSnippet = async (snippetName: string) => {
|
||||
const snippets = get(this).filter(snippet => snippet.name !== snippetName)
|
||||
const app = await API.saveAppMetadata(get(appStore).appId, { snippets })
|
||||
this.syncMetadata(app)
|
||||
}
|
||||
}
|
||||
|
||||
export const snippets = new SnippetStore()
|
|
@ -1,58 +0,0 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
import { ensureValidTheme, DefaultAppTheme } from "@budibase/shared-core"
|
||||
|
||||
export const createThemeStore = () => {
|
||||
const store = writable({
|
||||
theme: DefaultAppTheme,
|
||||
customTheme: {},
|
||||
})
|
||||
|
||||
const syncAppTheme = app => {
|
||||
store.update(state => {
|
||||
const theme = ensureValidTheme(app.theme, DefaultAppTheme)
|
||||
return {
|
||||
...state,
|
||||
theme,
|
||||
customTheme: app.customTheme,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const save = async (theme, appId) => {
|
||||
const app = await API.saveAppMetadata(appId, { theme })
|
||||
store.update(state => {
|
||||
state.theme = app.theme
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
const saveCustom = async (theme, appId) => {
|
||||
const updated = { ...get(store).customTheme, ...theme }
|
||||
const app = await API.saveAppMetadata(appId, { customTheme: updated })
|
||||
store.update(state => {
|
||||
state.customTheme = app.customTheme
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
const syncMetadata = metadata => {
|
||||
const { theme, customTheme } = metadata
|
||||
store.update(state => ({
|
||||
...state,
|
||||
theme: ensureValidTheme(theme, DefaultAppTheme),
|
||||
customTheme,
|
||||
}))
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
update: store.update,
|
||||
syncMetadata,
|
||||
syncAppTheme,
|
||||
save,
|
||||
saveCustom,
|
||||
}
|
||||
}
|
||||
|
||||
export const themeStore = createThemeStore()
|
|
@ -0,0 +1,58 @@
|
|||
import { get } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
import { ensureValidTheme, DefaultAppTheme } from "@budibase/shared-core"
|
||||
import { App, UpdateAppResponse, Theme, AppCustomTheme } from "@budibase/types"
|
||||
|
||||
interface ThemeState {
|
||||
theme: Theme
|
||||
customTheme: AppCustomTheme
|
||||
}
|
||||
|
||||
export class ThemeStore extends BudiStore<ThemeState> {
|
||||
constructor() {
|
||||
super({
|
||||
theme: DefaultAppTheme,
|
||||
customTheme: {},
|
||||
})
|
||||
}
|
||||
|
||||
syncAppTheme = (app: App) => {
|
||||
this.update(state => {
|
||||
const theme = ensureValidTheme(app.theme, DefaultAppTheme)
|
||||
return {
|
||||
...state,
|
||||
theme,
|
||||
customTheme: app.customTheme || {},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
save = async (theme: Theme, appId: string) => {
|
||||
const app = await API.saveAppMetadata(appId, { theme })
|
||||
this.update(state => ({
|
||||
...state,
|
||||
theme: ensureValidTheme(app.theme, DefaultAppTheme),
|
||||
}))
|
||||
}
|
||||
|
||||
saveCustom = async (theme: Partial<AppCustomTheme>, appId: string) => {
|
||||
const updated = { ...get(this).customTheme, ...theme }
|
||||
const app = await API.saveAppMetadata(appId, { customTheme: updated })
|
||||
this.update(state => ({
|
||||
...state,
|
||||
customTheme: app.customTheme || {},
|
||||
}))
|
||||
}
|
||||
|
||||
syncMetadata = (metadata: UpdateAppResponse) => {
|
||||
const { theme, customTheme } = metadata
|
||||
this.update(state => ({
|
||||
...state,
|
||||
theme: ensureValidTheme(theme, DefaultAppTheme),
|
||||
customTheme: customTheme || {},
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
export const themeStore = new ThemeStore()
|
|
@ -1,62 +0,0 @@
|
|||
import { writable, get, derived } from "svelte/store"
|
||||
|
||||
export const createUserStore = () => {
|
||||
const store = writable([])
|
||||
|
||||
const init = users => {
|
||||
store.set(users)
|
||||
}
|
||||
|
||||
const updateUser = user => {
|
||||
const $users = get(store)
|
||||
if (!$users.some(x => x.sessionId === user.sessionId)) {
|
||||
store.set([...$users, user])
|
||||
} else {
|
||||
store.update(state => {
|
||||
const index = state.findIndex(x => x.sessionId === user.sessionId)
|
||||
state[index] = user
|
||||
return state.slice()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const removeUser = sessionId => {
|
||||
store.update(state => {
|
||||
return state.filter(x => x.sessionId !== sessionId)
|
||||
})
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
store.set([])
|
||||
}
|
||||
|
||||
return {
|
||||
...store,
|
||||
actions: {
|
||||
init,
|
||||
updateUser,
|
||||
removeUser,
|
||||
reset,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const userStore = createUserStore()
|
||||
|
||||
export const userSelectedResourceMap = derived(userStore, $userStore => {
|
||||
let map = {}
|
||||
$userStore.forEach(user => {
|
||||
const resource = user.builderMetadata?.selectedResourceId
|
||||
if (resource) {
|
||||
if (!map[resource]) {
|
||||
map[resource] = []
|
||||
}
|
||||
map[resource].push(user)
|
||||
}
|
||||
})
|
||||
return map
|
||||
})
|
||||
|
||||
export const isOnlyUser = derived(userStore, $userStore => {
|
||||
return $userStore.length < 2
|
||||
})
|
|
@ -0,0 +1,59 @@
|
|||
import { get, derived } from "svelte/store"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
import { UIUser } from "@budibase/types"
|
||||
|
||||
export class UserStore extends BudiStore<UIUser[]> {
|
||||
constructor() {
|
||||
super([])
|
||||
}
|
||||
|
||||
init(users: UIUser[]) {
|
||||
this.set(users)
|
||||
}
|
||||
|
||||
updateUser(user: UIUser) {
|
||||
const $users = get(this)
|
||||
if (!$users.some(x => x.sessionId === user.sessionId)) {
|
||||
this.set([...$users, user])
|
||||
} else {
|
||||
this.update(state => {
|
||||
const index = state.findIndex(x => x.sessionId === user.sessionId)
|
||||
state[index] = user
|
||||
return state.slice()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
removeUser(sessionId: string) {
|
||||
this.update(state => {
|
||||
return state.filter(x => x.sessionId !== sessionId)
|
||||
})
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.set([])
|
||||
}
|
||||
}
|
||||
|
||||
export const userStore = new UserStore()
|
||||
|
||||
export const userSelectedResourceMap = derived(
|
||||
userStore,
|
||||
($userStore): Record<string, UIUser[]> => {
|
||||
let map: Record<string, UIUser[]> = {}
|
||||
$userStore.forEach(user => {
|
||||
const resource = user.builderMetadata?.selectedResourceId
|
||||
if (resource) {
|
||||
if (!map[resource]) {
|
||||
map[resource] = []
|
||||
}
|
||||
map[resource].push(user)
|
||||
}
|
||||
})
|
||||
return map
|
||||
}
|
||||
)
|
||||
|
||||
export const isOnlyUser = derived(userStore, $userStore => {
|
||||
return $userStore.length < 2
|
||||
})
|
|
@ -1,95 +0,0 @@
|
|||
import { createWebsocket } from "@budibase/frontend-core"
|
||||
import {
|
||||
automationStore,
|
||||
userStore,
|
||||
appStore,
|
||||
themeStore,
|
||||
navigationStore,
|
||||
deploymentStore,
|
||||
snippets,
|
||||
datasources,
|
||||
tables,
|
||||
roles,
|
||||
} from "@/stores/builder"
|
||||
import { get } from "svelte/store"
|
||||
import { auth, appsStore } from "@/stores/portal"
|
||||
import { screenStore } from "./screens"
|
||||
import { SocketEvent, BuilderSocketEvent, helpers } from "@budibase/shared-core"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
|
||||
export const createBuilderWebsocket = appId => {
|
||||
const socket = createWebsocket("/socket/builder")
|
||||
|
||||
// Built-in events
|
||||
socket.on("connect", () => {
|
||||
socket.emit(BuilderSocketEvent.SelectApp, { appId }, ({ users }) => {
|
||||
userStore.actions.init(users)
|
||||
})
|
||||
})
|
||||
socket.on("connect_error", err => {
|
||||
console.error("Failed to connect to builder websocket:", err.message)
|
||||
})
|
||||
socket.on("disconnect", () => {
|
||||
userStore.actions.reset()
|
||||
})
|
||||
|
||||
// User events
|
||||
socket.onOther(SocketEvent.UserUpdate, ({ user }) => {
|
||||
userStore.actions.updateUser(user)
|
||||
})
|
||||
socket.onOther(SocketEvent.UserDisconnect, ({ sessionId }) => {
|
||||
userStore.actions.removeUser(sessionId)
|
||||
})
|
||||
socket.onOther(BuilderSocketEvent.LockTransfer, ({ userId }) => {
|
||||
if (userId === get(auth)?.user?._id) {
|
||||
appStore.update(state => ({
|
||||
...state,
|
||||
hasLock: true,
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
// Data section events
|
||||
socket.onOther(BuilderSocketEvent.TableChange, ({ id, table }) => {
|
||||
tables.replaceTable(id, table)
|
||||
})
|
||||
socket.onOther(BuilderSocketEvent.DatasourceChange, ({ id, datasource }) => {
|
||||
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 }) => {
|
||||
appStore.syncMetadata(metadata)
|
||||
themeStore.syncMetadata(metadata)
|
||||
navigationStore.syncMetadata(metadata)
|
||||
snippets.syncMetadata(metadata)
|
||||
})
|
||||
socket.onOther(
|
||||
BuilderSocketEvent.AppPublishChange,
|
||||
async ({ user, published }) => {
|
||||
await appsStore.load()
|
||||
if (published) {
|
||||
await deploymentStore.load()
|
||||
}
|
||||
const verb = published ? "published" : "unpublished"
|
||||
notifications.success(`${helpers.getUserLabel(user)} ${verb} this app`)
|
||||
}
|
||||
)
|
||||
|
||||
// Automation events
|
||||
socket.onOther(BuilderSocketEvent.AutomationChange, ({ id, automation }) => {
|
||||
automationStore.actions.replace(id, automation)
|
||||
})
|
||||
|
||||
return socket
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
import { createWebsocket } from "@budibase/frontend-core"
|
||||
import {
|
||||
automationStore,
|
||||
userStore,
|
||||
appStore,
|
||||
themeStore,
|
||||
navigationStore,
|
||||
deploymentStore,
|
||||
snippets,
|
||||
datasources,
|
||||
tables,
|
||||
roles,
|
||||
} from "@/stores/builder"
|
||||
import { get } from "svelte/store"
|
||||
import { auth, appsStore } from "@/stores/portal"
|
||||
import { screenStore } from "./screens"
|
||||
import { SocketEvent, BuilderSocketEvent, helpers } from "@budibase/shared-core"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import { Automation, Datasource, Role, Table, UIUser } from "@budibase/types"
|
||||
|
||||
export const createBuilderWebsocket = (appId: string) => {
|
||||
const socket = createWebsocket("/socket/builder")
|
||||
|
||||
// Built-in events
|
||||
socket.on("connect", () => {
|
||||
socket.emit(
|
||||
BuilderSocketEvent.SelectApp,
|
||||
{ appId },
|
||||
({ users }: { users: UIUser[] }) => {
|
||||
userStore.init(users)
|
||||
}
|
||||
)
|
||||
})
|
||||
socket.on("connect_error", err => {
|
||||
console.error("Failed to connect to builder websocket:", err.message)
|
||||
})
|
||||
socket.on("disconnect", () => {
|
||||
userStore.reset()
|
||||
})
|
||||
|
||||
// User events
|
||||
socket.onOther(SocketEvent.UserUpdate, ({ user }: { user: UIUser }) => {
|
||||
userStore.updateUser(user)
|
||||
})
|
||||
socket.onOther(
|
||||
SocketEvent.UserDisconnect,
|
||||
({ sessionId }: { sessionId: string }) => {
|
||||
userStore.removeUser(sessionId)
|
||||
}
|
||||
)
|
||||
socket.onOther(
|
||||
BuilderSocketEvent.LockTransfer,
|
||||
({ userId }: { userId: string }) => {
|
||||
if (userId === get(auth)?.user?._id) {
|
||||
appStore.update(state => ({
|
||||
...state,
|
||||
hasLock: true,
|
||||
}))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Data section events
|
||||
socket.onOther(
|
||||
BuilderSocketEvent.TableChange,
|
||||
({ id, table }: { id: string; table: Table }) => {
|
||||
tables.replaceTable(id, table)
|
||||
}
|
||||
)
|
||||
socket.onOther(
|
||||
BuilderSocketEvent.DatasourceChange,
|
||||
({ id, datasource }: { id: string; datasource: Datasource }) => {
|
||||
datasources.replaceDatasource(id, datasource)
|
||||
}
|
||||
)
|
||||
|
||||
// Role events
|
||||
socket.onOther(
|
||||
BuilderSocketEvent.RoleChange,
|
||||
({ id, role }: { id: string; role: Role }) => {
|
||||
roles.replace(id, role)
|
||||
}
|
||||
)
|
||||
|
||||
// Design section events
|
||||
socket.onOther(
|
||||
BuilderSocketEvent.ScreenChange,
|
||||
({ id, screen }: { id: string; screen: Screen }) => {
|
||||
screenStore.replace(id, screen)
|
||||
}
|
||||
)
|
||||
|
||||
// App events
|
||||
socket.onOther(
|
||||
BuilderSocketEvent.AppMetadataChange,
|
||||
({ metadata }: { metadata: any }) => {
|
||||
appStore.syncMetadata(metadata)
|
||||
themeStore.syncMetadata(metadata)
|
||||
navigationStore.syncMetadata(metadata)
|
||||
snippets.syncMetadata(metadata)
|
||||
}
|
||||
)
|
||||
socket.onOther(
|
||||
BuilderSocketEvent.AppPublishChange,
|
||||
async ({ user, published }: { user: UIUser; published: boolean }) => {
|
||||
await appsStore.load()
|
||||
if (published) {
|
||||
await deploymentStore.load()
|
||||
}
|
||||
const verb = published ? "published" : "unpublished"
|
||||
notifications.success(`${helpers.getUserLabel(user)} ${verb} this app`)
|
||||
}
|
||||
)
|
||||
|
||||
// Automation events
|
||||
socket.onOther(
|
||||
BuilderSocketEvent.AutomationChange,
|
||||
({ id, automation }: { id: string; automation: Automation }) => {
|
||||
automationStore.actions.replace(id, automation)
|
||||
}
|
||||
)
|
||||
|
||||
return socket
|
||||
}
|
|
@ -1,19 +1,8 @@
|
|||
import { derived, Readable } from "svelte/store"
|
||||
import { auth } from "@/stores/portal"
|
||||
import { FeatureFlags, FeatureFlagDefaults } from "@budibase/types"
|
||||
|
||||
export const INITIAL_FEATUREFLAG_STATE = {
|
||||
SQS: false,
|
||||
DEFAULT_VALUES: false,
|
||||
BUDIBASE_AI: false,
|
||||
AI_CUSTOM_CONFIGS: false,
|
||||
}
|
||||
|
||||
export const featureFlags: Readable<Record<string, any>> = derived(
|
||||
[auth],
|
||||
([$auth]) => {
|
||||
return {
|
||||
...INITIAL_FEATUREFLAG_STATE,
|
||||
...($auth?.user?.flags || {}),
|
||||
}
|
||||
}
|
||||
)
|
||||
export const featureFlags: Readable<FeatureFlags> = derived(auth, $auth => ({
|
||||
...FeatureFlagDefaults,
|
||||
...($auth?.user?.flags || {}),
|
||||
}))
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
: RelationshipType.MANY_TO_MANY,
|
||||
}
|
||||
|
||||
async function searchFunction(searchParams) {
|
||||
async function searchFunction(_tableId, searchParams) {
|
||||
if (
|
||||
subtype !== BBReferenceFieldSubType.USER &&
|
||||
subtype !== BBReferenceFieldSubType.USERS
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
import { io } from "socket.io-client"
|
||||
import { io, Socket } from "socket.io-client"
|
||||
import { SocketEvent, SocketSessionTTL } from "@budibase/shared-core"
|
||||
import { APISessionID } from "../api"
|
||||
|
||||
const DefaultOptions = {
|
||||
heartbeat: true,
|
||||
}
|
||||
export interface ExtendedSocket extends Socket {
|
||||
onOther: (event: string, callback: (data: any) => void) => void
|
||||
}
|
||||
|
||||
export const createWebsocket = (path, options = DefaultOptions) => {
|
||||
export const createWebsocket = (
|
||||
path: string,
|
||||
options = DefaultOptions
|
||||
): ExtendedSocket => {
|
||||
if (!path) {
|
||||
throw "A websocket path must be provided"
|
||||
}
|
||||
|
@ -32,10 +38,10 @@ export const createWebsocket = (path, options = DefaultOptions) => {
|
|||
// Disable polling and rely on websocket only, as HTTP transport
|
||||
// will only work with sticky sessions which we don't have
|
||||
transports: ["websocket"],
|
||||
})
|
||||
}) as ExtendedSocket
|
||||
|
||||
// Set up a heartbeat that's half of the session TTL
|
||||
let interval
|
||||
let interval: NodeJS.Timeout | undefined
|
||||
if (heartbeat) {
|
||||
interval = setInterval(() => {
|
||||
socket.emit(SocketEvent.Heartbeat)
|
||||
|
@ -48,7 +54,7 @@ export const createWebsocket = (path, options = DefaultOptions) => {
|
|||
|
||||
// Helper utility to ignore events that were triggered due to API
|
||||
// changes made by us
|
||||
socket.onOther = (event, callback) => {
|
||||
socket.onOther = (event: string, callback: (data: any) => void) => {
|
||||
socket.on(event, data => {
|
||||
if (data?.apiSessionId !== APISessionID) {
|
||||
callback(data)
|
|
@ -1 +1 @@
|
|||
Subproject commit ae786121d923449b0ad5fcbd123d0a9fec28f65e
|
||||
Subproject commit 32d84f109d4edc526145472a7446327312151442
|
|
@ -163,9 +163,9 @@ export async function finaliseRow(
|
|||
contextRows: [enrichedRow],
|
||||
})
|
||||
const aiEnabled =
|
||||
((await features.flags.isEnabled(FeatureFlag.BUDIBASE_AI)) &&
|
||||
((await features.isEnabled(FeatureFlag.BUDIBASE_AI)) &&
|
||||
(await pro.features.isBudibaseAIEnabled())) ||
|
||||
((await features.flags.isEnabled(FeatureFlag.AI_CUSTOM_CONFIGS)) &&
|
||||
((await features.isEnabled(FeatureFlag.AI_CUSTOM_CONFIGS)) &&
|
||||
(await pro.features.isAICustomConfigsEnabled()))
|
||||
if (aiEnabled) {
|
||||
row = await processAIColumns(table, row, {
|
||||
|
|
|
@ -105,13 +105,13 @@ if (env.SELF_HOSTED) {
|
|||
export async function getActionDefinitions(): Promise<
|
||||
Record<keyof typeof AutomationActionStepId, AutomationStepDefinition>
|
||||
> {
|
||||
if (await features.flags.isEnabled(FeatureFlag.AUTOMATION_BRANCHING)) {
|
||||
if (await features.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))
|
||||
(await features.isEnabled(FeatureFlag.BUDIBASE_AI)) ||
|
||||
(await features.isEnabled(FeatureFlag.AI_CUSTOM_CONFIGS))
|
||||
) {
|
||||
BUILTIN_ACTION_DEFINITIONS["OPENAI"] = openai.definition
|
||||
}
|
||||
|
|
|
@ -100,10 +100,10 @@ export async function run({
|
|||
try {
|
||||
let response
|
||||
const customConfigsEnabled =
|
||||
(await features.flags.isEnabled(FeatureFlag.AI_CUSTOM_CONFIGS)) &&
|
||||
(await features.isEnabled(FeatureFlag.AI_CUSTOM_CONFIGS)) &&
|
||||
(await pro.features.isAICustomConfigsEnabled())
|
||||
const budibaseAIEnabled =
|
||||
(await features.flags.isEnabled(FeatureFlag.BUDIBASE_AI)) &&
|
||||
(await features.isEnabled(FeatureFlag.BUDIBASE_AI)) &&
|
||||
(await pro.features.isBudibaseAIEnabled())
|
||||
|
||||
let llmWrapper
|
||||
|
|
|
@ -7,7 +7,7 @@ import { fromZodError } from "zod-validation-error"
|
|||
function validate(schema: AnyZodObject, property: "body" | "params") {
|
||||
// Return a Koa middleware function
|
||||
return async (ctx: Ctx, next: any) => {
|
||||
if (!(await features.flags.isEnabled(FeatureFlag.USE_ZOD_VALIDATOR))) {
|
||||
if (!(await features.isEnabled(FeatureFlag.USE_ZOD_VALIDATOR))) {
|
||||
return next()
|
||||
}
|
||||
|
||||
|
|
|
@ -174,7 +174,9 @@ class QueryRunner {
|
|||
}
|
||||
|
||||
// needs to an array for next step
|
||||
if (!Array.isArray(rows)) {
|
||||
if (rows === null) {
|
||||
rows = []
|
||||
} else if (!Array.isArray(rows)) {
|
||||
rows = [rows]
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { FeatureFlags } from "@budibase/types"
|
||||
import { DevInfo, User } from "../../../documents"
|
||||
|
||||
export interface GenerateAPIKeyRequest {
|
||||
|
@ -8,5 +9,5 @@ export interface GenerateAPIKeyResponse extends DevInfo {}
|
|||
export interface FetchAPIKeyResponse extends DevInfo {}
|
||||
|
||||
export interface GetGlobalSelfResponse extends User {
|
||||
flags?: Record<string, any>
|
||||
flags?: FeatureFlags
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { User, Document, Plugin, Snippet } from "../"
|
||||
import { User, Document, Plugin, Snippet, Theme } from "../"
|
||||
import { SocketSession } from "../../sdk"
|
||||
|
||||
export type AppMetadataErrors = { [key: string]: string[] }
|
||||
|
@ -14,7 +14,7 @@ export interface App extends Document {
|
|||
instance: AppInstance
|
||||
tenantId: string
|
||||
status: string
|
||||
theme?: string
|
||||
theme?: Theme
|
||||
customTheme?: AppCustomTheme
|
||||
revertableVersion?: string
|
||||
lockedBy?: User
|
||||
|
@ -37,8 +37,8 @@ export interface AppInstance {
|
|||
|
||||
export interface AppNavigation {
|
||||
navigation: string
|
||||
title: string
|
||||
navWidth: string
|
||||
title?: string
|
||||
navWidth?: string
|
||||
sticky?: boolean
|
||||
hideLogo?: boolean
|
||||
logoUrl?: string
|
||||
|
@ -46,6 +46,7 @@ export interface AppNavigation {
|
|||
navBackground?: string
|
||||
navTextColor?: string
|
||||
links?: AppNavigationLink[]
|
||||
textAlign?: string
|
||||
}
|
||||
|
||||
export interface AppNavigationLink {
|
||||
|
@ -53,6 +54,8 @@ export interface AppNavigationLink {
|
|||
url: string
|
||||
id?: string
|
||||
roleId?: string
|
||||
type?: string
|
||||
subLinks?: AppNavigationLink[]
|
||||
}
|
||||
|
||||
export interface AppCustomTheme {
|
||||
|
|
|
@ -6,6 +6,12 @@ export enum FeatureFlag {
|
|||
USE_ZOD_VALIDATOR = "USE_ZOD_VALIDATOR",
|
||||
}
|
||||
|
||||
export interface TenantFeatureFlags {
|
||||
[key: string]: FeatureFlag[]
|
||||
export const FeatureFlagDefaults = {
|
||||
[FeatureFlag.DEFAULT_VALUES]: true,
|
||||
[FeatureFlag.AUTOMATION_BRANCHING]: true,
|
||||
[FeatureFlag.AI_CUSTOM_CONFIGS]: true,
|
||||
[FeatureFlag.BUDIBASE_AI]: true,
|
||||
[FeatureFlag.USE_ZOD_VALIDATOR]: false,
|
||||
}
|
||||
|
||||
export type FeatureFlags = typeof FeatureFlagDefaults
|
||||
|
|
|
@ -3,4 +3,5 @@ import { User } from "@budibase/types"
|
|||
export interface UIUser extends User {
|
||||
sessionId: string
|
||||
gridMetadata?: { focusedCellId?: string }
|
||||
builderMetadata?: { selectedResourceId?: string }
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
export * from "./integration"
|
||||
export * from "./misc"
|
||||
export * from "./automations"
|
||||
export * from "./grid"
|
||||
export * from "./preview"
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
// type purely to capture structures that the type is unknown, but maybe known later
|
||||
export type UIObject = Record<string, any>
|
|
@ -0,0 +1 @@
|
|||
export type PreviewDevice = "desktop" | "tablet" | "mobile"
|
|
@ -111,8 +111,7 @@ export async function getSelf(ctx: UserCtx<void, GetGlobalSelfResponse>) {
|
|||
ctx.body = await groups.enrichUserRolesFromGroups(user)
|
||||
|
||||
// add the feature flags for this tenant
|
||||
const flags = await features.flags.fetch()
|
||||
ctx.body.flags = flags
|
||||
ctx.body.flags = await features.flags.fetch()
|
||||
|
||||
addSessionAttributesToUser(ctx)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue