Merge branch 'master' into fix-trigger-automation

This commit is contained in:
Andrew Kingston 2025-01-07 09:58:01 +00:00 committed by GitHub
commit 4eb190f89d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 349 additions and 435 deletions

View File

@ -19,5 +19,8 @@ jobs:
cache: yarn
- run: yarn --frozen-lockfile
- name: Install OpenAPI pkg
run: yarn global add openapi
- name: update specs
run: cd packages/server && yarn specs && openapi specs/openapi.yaml --key=${{ secrets.README_API_KEY }} --id=6728a74f5918b50036c61841

View File

@ -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",

View File

@ -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"

View File

@ -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

View File

@ -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>
}

View File

@ -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()
}

View File

@ -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 {

View File

@ -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>> = {}

View File

@ -53,17 +53,24 @@ export class BudiStore<T> {
}
}
export class DerivedBudiStore<T, DerivedT extends T> extends BudiStore<T> {
// This deliberately does not extend a BudiStore as doing so imposes a requirement that
// DerivedT must extend T, which is not desirable, due to the type of the subscribe property.
export class DerivedBudiStore<T, DerivedT> {
store: BudiStore<T>
derivedStore: Readable<DerivedT>
subscribe: Readable<DerivedT>["subscribe"]
update: Writable<T>["update"]
set: Writable<T>["set"]
constructor(
init: T,
makeDerivedStore: (store: Writable<T>) => Readable<DerivedT>,
opts?: BudiStoreOpts
) {
super(init, opts)
this.store = new BudiStore(init, opts)
this.derivedStore = makeDerivedStore(this.store)
this.subscribe = this.derivedStore.subscribe
this.update = this.store.update
this.set = this.store.set
}
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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] ??= []

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -4,14 +4,14 @@ import { admin } from "@/stores/portal"
import analytics from "@/analytics"
import { BudiStore } from "@/stores/BudiStore"
import {
GetGlobalSelfResponse,
isSSOUser,
SetInitInfoRequest,
UpdateSelfRequest,
User,
} from "@budibase/types"
interface PortalAuthStore {
user?: User
user?: GetGlobalSelfResponse
initInfo?: Record<string, any>
accountPortalAccess: boolean
loaded: boolean
@ -33,7 +33,7 @@ class AuthStore extends BudiStore<PortalAuthStore> {
})
}
setUser(user?: User) {
setUser(user?: GetGlobalSelfResponse) {
this.set({
loaded: true,
user: user,

View File

@ -1,16 +0,0 @@
import { derived } from "svelte/store"
import { auth } from "@/stores/portal"
export const INITIAL_FEATUREFLAG_STATE = {
SQS: false,
DEFAULT_VALUES: false,
BUDIBASE_AI: false,
AI_CUSTOM_CONFIGS: false,
}
export const featureFlags = derived([auth], ([$auth]) => {
return {
...INITIAL_FEATUREFLAG_STATE,
...($auth?.user?.flags || {}),
}
})

View File

@ -0,0 +1,8 @@
import { derived, Readable } from "svelte/store"
import { auth } from "@/stores/portal"
import { FeatureFlags, FeatureFlagDefaults } from "@budibase/types"
export const featureFlags: Readable<FeatureFlags> = derived(auth, $auth => ({
...FeatureFlagDefaults,
...($auth?.user?.flags || {}),
}))

View File

@ -1,54 +0,0 @@
import { writable } from "svelte/store"
import { API } from "@/api"
import { licensing } from "./licensing"
import { ConfigType } from "@budibase/types"
export const createFeatureStore = () => {
const internalStore = writable({
scim: {
isFeatureFlagEnabled: false,
isConfigFlagEnabled: false,
},
})
const store = writable({
isScimEnabled: false,
})
internalStore.subscribe(s => {
store.update(state => ({
...state,
isScimEnabled: s.scim.isFeatureFlagEnabled && s.scim.isConfigFlagEnabled,
}))
})
licensing.subscribe(v => {
internalStore.update(state => ({
...state,
scim: {
...state.scim,
isFeatureFlagEnabled: v.scimEnabled,
},
}))
})
const actions = {
init: async () => {
const scimConfig = await API.getConfig(ConfigType.SCIM)
internalStore.update(state => ({
...state,
scim: {
...state.scim,
isConfigFlagEnabled: scimConfig?.config?.enabled,
},
}))
},
}
return {
subscribe: store.subscribe,
...actions,
}
}
export const features = createFeatureStore()

View File

@ -0,0 +1,35 @@
import { derived, Writable } from "svelte/store"
import { API } from "@/api"
import { licensing } from "./licensing"
import { ConfigType, isConfig, isSCIMConfig } from "@budibase/types"
import { DerivedBudiStore } from "../BudiStore"
interface FeatureState {
scimConfigEnabled: Boolean
}
interface DerivedFeatureState {
isScimEnabled: Boolean
}
class FeatureStore extends DerivedBudiStore<FeatureState, DerivedFeatureState> {
constructor() {
const makeDerivedStore = (store: Writable<FeatureState>) => {
return derived([store, licensing], ([$state, $licensing]) => ({
isScimEnabled: $state.scimConfigEnabled && $licensing.scimEnabled,
}))
}
super({ scimConfigEnabled: false }, makeDerivedStore)
}
async init() {
const config = await API.getConfig(ConfigType.SCIM)
this.update(state => ({
...state,
scimConfigEnabled:
isConfig(config) && isSCIMConfig(config) && config.config.enabled,
}))
}
}
export const features = new FeatureStore()

View File

@ -16,6 +16,7 @@
},
"dependencies": {
"@budibase/backend-core": "*",
"@budibase/pouchdb-replication-stream": "1.2.11",
"@budibase/string-templates": "*",
"@budibase/types": "*",
"chalk": "4.1.0",
@ -28,9 +29,9 @@
"inquirer": "8.0.0",
"lookpath": "1.1.0",
"node-fetch": "2.6.7",
"open": "8.4.2",
"posthog-node": "4.0.1",
"pouchdb": "7.3.0",
"@budibase/pouchdb-replication-stream": "1.2.11",
"randomstring": "1.1.5",
"tar": "6.2.1",
"yaml": "^2.1.1"

View File

@ -3,6 +3,8 @@ import { info, success } from "../utils"
import * as makeFiles from "./makeFiles"
import compose from "docker-compose"
import fs from "fs"
import { confirmation } from "../questions"
const open = require("open")
export async function start() {
await checkDockerConfigured()
@ -22,6 +24,9 @@ export async function start() {
// need to log as it makes it more clear
await compose.upAll({ cwd: "./", log: true })
})
if (await confirmation(`Do you wish to open http://localhost:${port} ?`)) {
await open(`http://localhost:${port}`)
}
console.log(
success(
`Services started, please go to http://localhost:${port} for next steps.`

@ -1 +1 @@
Subproject commit ae786121d923449b0ad5fcbd123d0a9fec28f65e
Subproject commit 32d84f109d4edc526145472a7446327312151442

View File

@ -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, {

View File

@ -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
}

View File

@ -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

View File

@ -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()
}

View File

@ -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
}

View File

@ -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 {

View File

@ -127,6 +127,9 @@ export interface AIInnerConfig {
export interface AIConfig extends Config<AIInnerConfig> {}
export const isConfig = (config: Object): config is Config =>
"type" in config && "config" in config
export const isSettingsConfig = (config: Config): config is SettingsConfig =>
config.type === ConfigType.SETTINGS

View File

@ -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

View File

@ -1,4 +1,5 @@
export * from "./integration"
export * from "./misc"
export * from "./automations"
export * from "./grid"
export * from "./preview"

View File

@ -0,0 +1,2 @@
// type purely to capture structures that the type is unknown, but maybe known later
export type UIObject = Record<string, any>

View File

@ -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)
}

View File

@ -2131,9 +2131,9 @@
through2 "^2.0.0"
"@budibase/pro@npm:@budibase/pro@latest":
version "3.2.28"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-3.2.28.tgz#59b5b37225715bb8fbf5b1c5c989140b10b58710"
integrity sha512-eDPeZpYFRZYQhCulcQAUwFoPk68c8+K9mIsB6QD3oMHmHTDA1P2ZcXvLNqDTIqHB94DqnWinqDf4hTuGYApgPA==
version "3.2.32"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-3.2.32.tgz#f6abcd5a5524e7f33d958acb6e610e29995427bb"
integrity sha512-bF0pd17IjYugjll2yKYmb0RM+tfKZcCmRBc4XG2NZ4f/I47QaOovm9RqSw6tfqCFuzRewxR3SWmtmSseUc/e0w==
dependencies:
"@anthropic-ai/sdk" "^0.27.3"
"@budibase/backend-core" "*"
@ -15600,15 +15600,7 @@ only@~0.0.2:
resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4"
integrity sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==
open@^7.3.1:
version "7.4.2"
resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321"
integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==
dependencies:
is-docker "^2.0.0"
is-wsl "^2.1.1"
open@^8.0.0, open@^8.4.0, open@~8.4.0:
open@8.4.2, open@^8.0.0, open@^8.4.0, open@~8.4.0:
version "8.4.2"
resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9"
integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==
@ -15617,6 +15609,14 @@ open@^8.0.0, open@^8.4.0, open@~8.4.0:
is-docker "^2.1.1"
is-wsl "^2.2.0"
open@^7.3.1:
version "7.4.2"
resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321"
integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==
dependencies:
is-docker "^2.0.0"
is-wsl "^2.1.1"
openai@4.59.0:
version "4.59.0"
resolved "https://registry.yarnpkg.com/openai/-/openai-4.59.0.tgz#3961d11a9afb5920e1bd475948a87969e244fc08"