Merge remote-tracking branch 'origin/master' into builder-store-roles
This commit is contained in:
commit
f3aff12955
|
@ -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"
|
||||
|
|
|
@ -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>> = {}
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
async function saveTemplate() {
|
||||
try {
|
||||
// Save your template config
|
||||
await email.templates.save(selectedTemplate)
|
||||
await email.saveTemplate(selectedTemplate)
|
||||
notifications.success("Template saved")
|
||||
} catch (error) {
|
||||
notifications.error("Failed to update template settings")
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await email.templates.fetch()
|
||||
await email.fetchTemplates()
|
||||
} catch (error) {
|
||||
notifications.error("Error fetching email templates")
|
||||
}
|
||||
|
|
|
@ -10,7 +10,8 @@
|
|||
let deleteDialog
|
||||
|
||||
const save = async data => {
|
||||
await environment.updateVariable(data)
|
||||
const { name, ...rest } = data
|
||||
await environment.updateVariable(name, rest)
|
||||
editVariableModal.hide()
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -66,7 +66,7 @@ export class RowActionStore extends BudiStore<RowActionState> {
|
|||
}
|
||||
|
||||
if (!name) {
|
||||
return
|
||||
throw new Error("Failed to generate a unique name for the row action")
|
||||
}
|
||||
|
||||
// Create the action
|
||||
|
|
|
@ -12,7 +12,7 @@ interface ThemeState {
|
|||
export class ThemeStore extends BudiStore<ThemeState> {
|
||||
constructor() {
|
||||
super({
|
||||
theme: DefaultAppTheme as Theme,
|
||||
theme: DefaultAppTheme,
|
||||
customTheme: {},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
import { writable } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
|
||||
export function createBackupsStore() {
|
||||
const store = writable({})
|
||||
|
||||
function selectBackup(backupId) {
|
||||
store.update(state => {
|
||||
state.selectedBackup = backupId
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
async function searchBackups(appId, opts) {
|
||||
return API.searchBackups(appId, opts)
|
||||
}
|
||||
|
||||
async function restoreBackup(appId, backupId, name) {
|
||||
return API.restoreBackup(appId, backupId, name)
|
||||
}
|
||||
|
||||
async function deleteBackup(appId, backupId) {
|
||||
return API.deleteBackup(appId, backupId)
|
||||
}
|
||||
|
||||
async function createManualBackup(appId) {
|
||||
return API.createManualBackup(appId)
|
||||
}
|
||||
|
||||
return {
|
||||
createManualBackup,
|
||||
searchBackups,
|
||||
selectBackup,
|
||||
deleteBackup,
|
||||
restoreBackup,
|
||||
subscribe: store.subscribe,
|
||||
}
|
||||
}
|
||||
|
||||
export const backups = createBackupsStore()
|
|
@ -1,5 +1,5 @@
|
|||
import { it, expect, describe, beforeEach, vi } from "vitest"
|
||||
import { createBackupsStore } from "./backups"
|
||||
import { BackupStore } from "./backups"
|
||||
import { writable } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
|
||||
|
@ -33,7 +33,7 @@ describe("backups store", () => {
|
|||
ctx.writableReturn = { update: vi.fn(), subscribe: vi.fn() }
|
||||
writable.mockReturnValue(ctx.writableReturn)
|
||||
|
||||
ctx.returnedStore = createBackupsStore()
|
||||
ctx.returnedStore = new BackupStore()
|
||||
})
|
||||
|
||||
it("inits the writable store with the default config", () => {
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import { API } from "@/api"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
import { SearchAppBackupsRequest } from "@budibase/types"
|
||||
|
||||
interface BackupState {
|
||||
selectedBackup?: string
|
||||
}
|
||||
|
||||
export class BackupStore extends BudiStore<BackupState> {
|
||||
constructor() {
|
||||
super({})
|
||||
}
|
||||
|
||||
selectBackup(backupId: string) {
|
||||
this.update(state => {
|
||||
state.selectedBackup = backupId
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
async searchBackups(appId: string, opts: SearchAppBackupsRequest) {
|
||||
return API.searchBackups(appId, opts)
|
||||
}
|
||||
|
||||
async restoreBackup(appId: string, backupId: string, name?: string) {
|
||||
return API.restoreBackup(appId, backupId, name)
|
||||
}
|
||||
|
||||
async deleteBackup(appId: string, backupId: string) {
|
||||
return API.deleteBackup(appId, backupId)
|
||||
}
|
||||
|
||||
async createManualBackup(appId: string) {
|
||||
return API.createManualBackup(appId)
|
||||
}
|
||||
}
|
||||
|
||||
export const backups = new BackupStore()
|
|
@ -1,36 +0,0 @@
|
|||
import { writable } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
|
||||
export function createEmailStore() {
|
||||
const store = writable({})
|
||||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
templates: {
|
||||
fetch: async () => {
|
||||
// Fetch the email template definitions and templates
|
||||
const definitions = await API.getEmailTemplateDefinitions()
|
||||
const templates = await API.getEmailTemplates()
|
||||
store.set({
|
||||
definitions,
|
||||
templates,
|
||||
})
|
||||
},
|
||||
save: async template => {
|
||||
// Save your template config
|
||||
const savedTemplate = await API.saveEmailTemplate(template)
|
||||
template._rev = savedTemplate._rev
|
||||
template._id = savedTemplate._id
|
||||
store.update(state => {
|
||||
const currentIdx = state.templates.findIndex(
|
||||
template => template.purpose === savedTemplate.purpose
|
||||
)
|
||||
state.templates.splice(currentIdx, 1, template)
|
||||
return state
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const email = createEmailStore()
|
|
@ -0,0 +1,43 @@
|
|||
import { API } from "@/api"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
import {
|
||||
FetchGlobalTemplateDefinitionResponse,
|
||||
Template,
|
||||
} from "@budibase/types"
|
||||
|
||||
interface EmailState {
|
||||
definitions?: FetchGlobalTemplateDefinitionResponse
|
||||
templates: Template[]
|
||||
}
|
||||
|
||||
class EmailStore extends BudiStore<EmailState> {
|
||||
constructor() {
|
||||
super({
|
||||
templates: [],
|
||||
})
|
||||
}
|
||||
|
||||
async fetchTemplates() {
|
||||
const definitions = await API.getEmailTemplateDefinitions()
|
||||
const templates = await API.getEmailTemplates()
|
||||
this.set({
|
||||
definitions,
|
||||
templates,
|
||||
})
|
||||
}
|
||||
|
||||
async saveTemplate(template: Template) {
|
||||
const savedTemplate = await API.saveEmailTemplate(template)
|
||||
template._rev = savedTemplate._rev
|
||||
template._id = savedTemplate._id
|
||||
this.update(state => {
|
||||
const currentIdx = state.templates.findIndex(
|
||||
template => template.purpose === savedTemplate.purpose
|
||||
)
|
||||
state.templates.splice(currentIdx, 1, template)
|
||||
return state
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const email = new EmailStore()
|
|
@ -1,71 +0,0 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
import { licensing } from "@/stores/portal"
|
||||
|
||||
export function createEnvironmentStore() {
|
||||
const { subscribe, update } = writable({
|
||||
variables: [],
|
||||
status: {},
|
||||
})
|
||||
|
||||
async function checkStatus() {
|
||||
const status = await API.checkEnvironmentVariableStatus()
|
||||
update(store => {
|
||||
store.status = status
|
||||
return store
|
||||
})
|
||||
}
|
||||
|
||||
async function loadVariables() {
|
||||
if (get(licensing).environmentVariablesEnabled) {
|
||||
const envVars = await API.fetchEnvironmentVariables()
|
||||
const mappedVars = envVars.variables.map(name => ({ name }))
|
||||
update(store => {
|
||||
store.variables = mappedVars
|
||||
return store
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function createVariable(data) {
|
||||
await API.createEnvironmentVariable(data)
|
||||
let mappedVar = { name: data.name }
|
||||
update(store => {
|
||||
store.variables = [mappedVar, ...store.variables]
|
||||
return store
|
||||
})
|
||||
}
|
||||
|
||||
async function deleteVariable(varName) {
|
||||
await API.deleteEnvironmentVariable(varName)
|
||||
update(store => {
|
||||
store.variables = store.variables.filter(
|
||||
envVar => envVar.name !== varName
|
||||
)
|
||||
return store
|
||||
})
|
||||
}
|
||||
|
||||
async function updateVariable(data) {
|
||||
await API.updateEnvironmentVariable(data)
|
||||
}
|
||||
|
||||
async function upgradePanelOpened() {
|
||||
await API.publishEvent(
|
||||
Constants.EventPublishType.ENV_VAR_UPGRADE_PANEL_OPENED
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
checkStatus,
|
||||
loadVariables,
|
||||
createVariable,
|
||||
deleteVariable,
|
||||
updateVariable,
|
||||
upgradePanelOpened,
|
||||
}
|
||||
}
|
||||
|
||||
export const environment = createEnvironmentStore()
|
|
@ -0,0 +1,79 @@
|
|||
import { get } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
import { licensing } from "@/stores/portal"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
import {
|
||||
CreateEnvironmentVariableRequest,
|
||||
EventPublishType,
|
||||
StatusEnvironmentVariableResponse,
|
||||
UpdateEnvironmentVariableRequest,
|
||||
} from "@budibase/types"
|
||||
|
||||
type EnvVar = {
|
||||
name: string
|
||||
}
|
||||
|
||||
interface EnvironmentState {
|
||||
variables: EnvVar[]
|
||||
status: StatusEnvironmentVariableResponse
|
||||
}
|
||||
|
||||
class EnvironmentStore extends BudiStore<EnvironmentState> {
|
||||
constructor() {
|
||||
super({
|
||||
variables: [],
|
||||
status: {
|
||||
encryptionKeyAvailable: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async checkStatus() {
|
||||
const status = await API.checkEnvironmentVariableStatus()
|
||||
this.update(store => {
|
||||
store.status = status
|
||||
return store
|
||||
})
|
||||
}
|
||||
|
||||
async loadVariables() {
|
||||
if (get(licensing).environmentVariablesEnabled) {
|
||||
const envVars: string[] = (await API.fetchEnvironmentVariables())
|
||||
.variables
|
||||
const mappedVars = envVars.map(name => ({ name }))
|
||||
this.update(store => {
|
||||
store.variables = mappedVars
|
||||
return store
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async createVariable(data: CreateEnvironmentVariableRequest) {
|
||||
await API.createEnvironmentVariable(data)
|
||||
let mappedVar = { name: data.name }
|
||||
this.update(state => {
|
||||
state.variables = [mappedVar, ...state.variables]
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
async deleteVariable(name: string) {
|
||||
await API.deleteEnvironmentVariable(name)
|
||||
this.update(state => {
|
||||
state.variables = state.variables.filter(envVar => envVar.name !== name)
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
async updateVariable(name: string, data: UpdateEnvironmentVariableRequest) {
|
||||
await API.updateEnvironmentVariable(name, data)
|
||||
}
|
||||
|
||||
async upgradePanelOpened() {
|
||||
await API.publishEvent(
|
||||
EventPublishType.ENVIRONMENT_VARIABLE_UPGRADE_PANEL_OPENED
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const environment = new EnvironmentStore()
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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, string>
|
||||
flags?: FeatureFlags
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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