diff --git a/examples/nextjs-api-sales/package.json b/examples/nextjs-api-sales/package.json index 1050c6b75e..d50e7d4eb5 100644 --- a/examples/nextjs-api-sales/package.json +++ b/examples/nextjs-api-sales/package.json @@ -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", diff --git a/examples/nextjs-api-sales/yarn.lock b/examples/nextjs-api-sales/yarn.lock index b595a148bf..a10fccb2e9 100644 --- a/examples/nextjs-api-sales/yarn.lock +++ b/examples/nextjs-api-sales/yarn.lock @@ -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" diff --git a/lerna.json b/lerna.json index 5e0bc09825..dde9cf03a0 100644 --- a/lerna.json +++ b/lerna.json @@ -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": { diff --git a/packages/backend-core/src/context/mainContext.ts b/packages/backend-core/src/context/mainContext.ts index 64ba240fa5..e5f20882d3 100644 --- a/packages/backend-core/src/context/mainContext.ts +++ b/packages/backend-core/src/context/mainContext.ts @@ -385,17 +385,17 @@ export function getCurrentContext(): ContextMap | undefined { } } -export function getFeatureFlags>( +export function getFeatureFlags( key: string -): T | undefined { +): Record | 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) { +export function setFeatureFlags(key: string, value: Record) { const context = getCurrentContext() if (!context) { return diff --git a/packages/backend-core/src/context/types.ts b/packages/backend-core/src/context/types.ts index 5549a47ff7..23598b951e 100644 --- a/packages/backend-core/src/context/types.ts +++ b/packages/backend-core/src/context/types.ts @@ -20,7 +20,7 @@ export type ContextMap = { clients: Record } featureFlagCache?: { - [key: string]: Record + [key: string]: Record } viewToTableCache?: Record } diff --git a/packages/backend-core/src/features/features.ts b/packages/backend-core/src/features/features.ts index 650254fcb2..772bcf5860 100644 --- a/packages/backend-core/src/features/features.ts +++ b/packages/backend-core/src/features/features.ts @@ -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 { - static boolean(defaultValue: boolean): Flag { - return new BooleanFlag(defaultValue) - } - - static string(defaultValue: string): Flag { - return new StringFlag(defaultValue) - } - - static number(defaultValue: number): Flag { - return new NumberFlag(defaultValue) - } - - protected constructor(public defaultValue: T) {} - - abstract parse(value: any): T -} - -type UnwrapFlag = F extends Flag ? U : never - -export type FlagValues = { - [K in keyof T]: UnwrapFlag -} - -type KeysOfType = { - [K in keyof T]: T[K] extends Flag ? K : never -}[keyof T] - -class BooleanFlag extends Flag { - 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 { - parse(value: any) { - if (typeof value === "string") { - return value - } - throw new Error(`could not parse value "${value}" as string`) - } -} - -class NumberFlag extends Flag { - 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, T extends { [key: string]: V }> { +export class FlagSet { // 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, T extends { [key: string]: V }> { this.setId = crypto.randomUUID() } - defaults(): FlagValues { - return Object.keys(this.flagSchema).reduce((acc, key) => { - const typedKey = key as keyof T - acc[typedKey] = this.flagSchema[key].defaultValue - return acc - }, {} as FlagValues) + 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(key: K): Promise[K]> { + async isEnabled(key: K): Promise { const flags = await this.fetch() return flags[key] } - async isEnabled>(key: K): Promise { - const flags = await this.fetch() - return flags[key] - } - - async fetch(): Promise> { + async fetch(): Promise { return await tracer.trace("features.fetch", async span => { - const cachedFlags = context.getFeatureFlags>(this.setId) + const cachedFlags = context.getFeatureFlags(this.setId) if (cachedFlags) { span?.addTags({ fromCache: true }) - return cachedFlags + return cachedFlags as T } const tags: Record = {} @@ -189,7 +113,7 @@ export class FlagSet, 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, T extends { [key: string]: V }> { tags[`readFromPostHog`] = true const personProperties: Record = { 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, 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, 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.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 extends Promise ? U : T -export type FeatureFlags = UnwrapPromise> +export async function isEnabled(flag: keyof typeof FeatureFlagDefaults) { + return await flags.isEnabled(flag) +} + +export async function all() { + return await flags.fetch() +} diff --git a/packages/backend-core/src/features/tests/features.spec.ts b/packages/backend-core/src/features/tests/features.spec.ts index ced874f4af..f918347eea 100644 --- a/packages/backend-core/src/features/tests/features.spec.ts +++ b/packages/backend-core/src/features/tests/features.spec.ts @@ -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 environmentFlags?: string posthogFlags?: PostHogFlags - expected?: Partial> + expected?: Partial 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 { diff --git a/packages/backend-core/src/features/tests/utils.ts b/packages/backend-core/src/features/tests/utils.ts index cc633c083d..b9281b7f19 100644 --- a/packages/backend-core/src/features/tests/utils.ts +++ b/packages/backend-core/src/features/tests/utils.ts @@ -1,5 +1,6 @@ -import { FeatureFlags, parseEnvFlags } from ".." +import { FeatureFlags } from "@budibase/types" import { setEnv } from "../../environment" +import { parseEnvFlags } from "../features" function getCurrentFlags(): Record> { const result: Record> = {} diff --git a/packages/builder/src/stores/builder/automations.ts b/packages/builder/src/stores/builder/automations.ts index c43a856cc4..9b20b4cd03 100644 --- a/packages/builder/src/stores/builder/automations.ts +++ b/packages/builder/src/stores/builder/automations.ts @@ -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 diff --git a/packages/builder/src/stores/builder/flags.js b/packages/builder/src/stores/builder/flags.js deleted file mode 100644 index 21236ffb01..0000000000 --- a/packages/builder/src/stores/builder/flags.js +++ /dev/null @@ -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() diff --git a/packages/builder/src/stores/builder/flags.ts b/packages/builder/src/stores/builder/flags.ts new file mode 100644 index 0000000000..6d2150f49b --- /dev/null +++ b/packages/builder/src/stores/builder/flags.ts @@ -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 { + 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() diff --git a/packages/builder/src/stores/builder/hover.js b/packages/builder/src/stores/builder/hover.ts similarity index 73% rename from packages/builder/src/stores/builder/hover.js rename to packages/builder/src/stores/builder/hover.ts index e28c74eaf2..8f9b77075c 100644 --- a/packages/builder/src/stores/builder/hover.js +++ b/packages/builder/src/stores/builder/hover.ts @@ -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 { + 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 } diff --git a/packages/builder/src/stores/builder/layouts.js b/packages/builder/src/stores/builder/layouts.ts similarity index 78% rename from packages/builder/src/stores/builder/layouts.js rename to packages/builder/src/stores/builder/layouts.ts index 7617e203f0..af7b17fcc7 100644 --- a/packages/builder/src/stores/builder/layouts.js +++ b/packages/builder/src/stores/builder/layouts.ts @@ -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 { 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) diff --git a/packages/builder/src/stores/builder/navigation.js b/packages/builder/src/stores/builder/navigation.ts similarity index 73% rename from packages/builder/src/stores/builder/navigation.js rename to packages/builder/src/stores/builder/navigation.ts index e9a5e6f9dd..1574efee2a 100644 --- a/packages/builder/src/stores/builder/navigation.js +++ b/packages/builder/src/stores/builder/navigation.ts @@ -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 { 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) } diff --git a/packages/builder/src/stores/builder/preview.js b/packages/builder/src/stores/builder/preview.js deleted file mode 100644 index 4923185ee7..0000000000 --- a/packages/builder/src/stores/builder/preview.js +++ /dev/null @@ -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() diff --git a/packages/builder/src/stores/builder/preview.ts b/packages/builder/src/stores/builder/preview.ts new file mode 100644 index 0000000000..fe14c02535 --- /dev/null +++ b/packages/builder/src/stores/builder/preview.ts @@ -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 + +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 { + 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() diff --git a/packages/builder/src/stores/builder/rowActions.js b/packages/builder/src/stores/builder/rowActions.ts similarity index 70% rename from packages/builder/src/stores/builder/rowActions.js rename to packages/builder/src/stores/builder/rowActions.ts index 9cc4063b91..9576eccd1b 100644 --- a/packages/builder/src/stores/builder/rowActions.js +++ b/packages/builder/src/stores/builder/rowActions.ts @@ -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 { 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(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] ??= [] diff --git a/packages/builder/src/stores/builder/snippets.js b/packages/builder/src/stores/builder/snippets.js deleted file mode 100644 index 4e98ef1bdc..0000000000 --- a/packages/builder/src/stores/builder/snippets.js +++ /dev/null @@ -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() diff --git a/packages/builder/src/stores/builder/snippets.ts b/packages/builder/src/stores/builder/snippets.ts new file mode 100644 index 0000000000..a6a63f7c89 --- /dev/null +++ b/packages/builder/src/stores/builder/snippets.ts @@ -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 { + 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() diff --git a/packages/builder/src/stores/builder/theme.js b/packages/builder/src/stores/builder/theme.js deleted file mode 100644 index ed46e9095a..0000000000 --- a/packages/builder/src/stores/builder/theme.js +++ /dev/null @@ -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() diff --git a/packages/builder/src/stores/builder/theme.ts b/packages/builder/src/stores/builder/theme.ts new file mode 100644 index 0000000000..604add9410 --- /dev/null +++ b/packages/builder/src/stores/builder/theme.ts @@ -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 { + 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, 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() diff --git a/packages/builder/src/stores/builder/users.js b/packages/builder/src/stores/builder/users.js deleted file mode 100644 index 0794c63289..0000000000 --- a/packages/builder/src/stores/builder/users.js +++ /dev/null @@ -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 -}) diff --git a/packages/builder/src/stores/builder/users.ts b/packages/builder/src/stores/builder/users.ts new file mode 100644 index 0000000000..5350b2af90 --- /dev/null +++ b/packages/builder/src/stores/builder/users.ts @@ -0,0 +1,59 @@ +import { get, derived } from "svelte/store" +import { BudiStore } from "../BudiStore" +import { UIUser } from "@budibase/types" + +export class UserStore extends BudiStore { + 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 => { + let map: Record = {} + $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 +}) diff --git a/packages/builder/src/stores/builder/websocket.js b/packages/builder/src/stores/builder/websocket.js deleted file mode 100644 index 3944c8cba5..0000000000 --- a/packages/builder/src/stores/builder/websocket.js +++ /dev/null @@ -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 -} diff --git a/packages/builder/src/stores/builder/websocket.ts b/packages/builder/src/stores/builder/websocket.ts new file mode 100644 index 0000000000..bd9e2c8d4d --- /dev/null +++ b/packages/builder/src/stores/builder/websocket.ts @@ -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 +} diff --git a/packages/builder/src/stores/portal/featureFlags.ts b/packages/builder/src/stores/portal/featureFlags.ts index ac071f1169..0e40d0c7b4 100644 --- a/packages/builder/src/stores/portal/featureFlags.ts +++ b/packages/builder/src/stores/portal/featureFlags.ts @@ -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> = derived( - [auth], - ([$auth]) => { - return { - ...INITIAL_FEATUREFLAG_STATE, - ...($auth?.user?.flags || {}), - } - } -) +export const featureFlags: Readable = derived(auth, $auth => ({ + ...FeatureFlagDefaults, + ...($auth?.user?.flags || {}), +})) diff --git a/packages/frontend-core/src/components/grid/cells/BBReferenceCell.svelte b/packages/frontend-core/src/components/grid/cells/BBReferenceCell.svelte index 5d98ba903b..fe6d8945ba 100644 --- a/packages/frontend-core/src/components/grid/cells/BBReferenceCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/BBReferenceCell.svelte @@ -26,7 +26,7 @@ : RelationshipType.MANY_TO_MANY, } - async function searchFunction(searchParams) { + async function searchFunction(_tableId, searchParams) { if ( subtype !== BBReferenceFieldSubType.USER && subtype !== BBReferenceFieldSubType.USERS diff --git a/packages/frontend-core/src/utils/websocket.js b/packages/frontend-core/src/utils/websocket.ts similarity index 78% rename from packages/frontend-core/src/utils/websocket.js rename to packages/frontend-core/src/utils/websocket.ts index dee679eaef..475b14176f 100644 --- a/packages/frontend-core/src/utils/websocket.js +++ b/packages/frontend-core/src/utils/websocket.ts @@ -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) diff --git a/packages/pro b/packages/pro index ae786121d9..32d84f109d 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit ae786121d923449b0ad5fcbd123d0a9fec28f65e +Subproject commit 32d84f109d4edc526145472a7446327312151442 diff --git a/packages/server/src/api/controllers/row/staticFormula.ts b/packages/server/src/api/controllers/row/staticFormula.ts index 4464b7f44a..b81a164807 100644 --- a/packages/server/src/api/controllers/row/staticFormula.ts +++ b/packages/server/src/api/controllers/row/staticFormula.ts @@ -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, { diff --git a/packages/server/src/automations/actions.ts b/packages/server/src/automations/actions.ts index e5a1c63b7d..1c201d1f64 100644 --- a/packages/server/src/automations/actions.ts +++ b/packages/server/src/automations/actions.ts @@ -105,13 +105,13 @@ if (env.SELF_HOSTED) { export async function getActionDefinitions(): Promise< Record > { - 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 } diff --git a/packages/server/src/automations/steps/openai.ts b/packages/server/src/automations/steps/openai.ts index 48eaa93057..19595cc0d0 100644 --- a/packages/server/src/automations/steps/openai.ts +++ b/packages/server/src/automations/steps/openai.ts @@ -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 diff --git a/packages/server/src/middleware/zod-validator.ts b/packages/server/src/middleware/zod-validator.ts index e8cc2c470a..d57e1c48ff 100644 --- a/packages/server/src/middleware/zod-validator.ts +++ b/packages/server/src/middleware/zod-validator.ts @@ -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() } diff --git a/packages/server/src/threads/query.ts b/packages/server/src/threads/query.ts index facdd20642..3ba4995b2c 100644 --- a/packages/server/src/threads/query.ts +++ b/packages/server/src/threads/query.ts @@ -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] } diff --git a/packages/types/src/api/web/global/self.ts b/packages/types/src/api/web/global/self.ts index c478e89dd6..9e879e6c3c 100644 --- a/packages/types/src/api/web/global/self.ts +++ b/packages/types/src/api/web/global/self.ts @@ -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 + flags?: FeatureFlags } diff --git a/packages/types/src/documents/app/app.ts b/packages/types/src/documents/app/app.ts index e31dd1e9ac..46799cb61a 100644 --- a/packages/types/src/documents/app/app.ts +++ b/packages/types/src/documents/app/app.ts @@ -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 { diff --git a/packages/types/src/sdk/featureFlag.ts b/packages/types/src/sdk/featureFlag.ts index 98e744324c..725ae0feb1 100644 --- a/packages/types/src/sdk/featureFlag.ts +++ b/packages/types/src/sdk/featureFlag.ts @@ -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 diff --git a/packages/types/src/ui/stores/grid/user.ts b/packages/types/src/ui/stores/grid/user.ts index b6eb529805..9625b0bb80 100644 --- a/packages/types/src/ui/stores/grid/user.ts +++ b/packages/types/src/ui/stores/grid/user.ts @@ -3,4 +3,5 @@ import { User } from "@budibase/types" export interface UIUser extends User { sessionId: string gridMetadata?: { focusedCellId?: string } + builderMetadata?: { selectedResourceId?: string } } diff --git a/packages/types/src/ui/stores/index.ts b/packages/types/src/ui/stores/index.ts index 8dae68862e..daf73bba75 100644 --- a/packages/types/src/ui/stores/index.ts +++ b/packages/types/src/ui/stores/index.ts @@ -1,3 +1,5 @@ export * from "./integration" +export * from "./misc" export * from "./automations" export * from "./grid" +export * from "./preview" diff --git a/packages/types/src/ui/stores/misc.ts b/packages/types/src/ui/stores/misc.ts new file mode 100644 index 0000000000..275b388e9f --- /dev/null +++ b/packages/types/src/ui/stores/misc.ts @@ -0,0 +1,2 @@ +// type purely to capture structures that the type is unknown, but maybe known later +export type UIObject = Record diff --git a/packages/types/src/ui/stores/preview.ts b/packages/types/src/ui/stores/preview.ts new file mode 100644 index 0000000000..4d09366ff5 --- /dev/null +++ b/packages/types/src/ui/stores/preview.ts @@ -0,0 +1 @@ +export type PreviewDevice = "desktop" | "tablet" | "mobile" diff --git a/packages/worker/src/api/controllers/global/self.ts b/packages/worker/src/api/controllers/global/self.ts index f8488f526b..eb704ccf02 100644 --- a/packages/worker/src/api/controllers/global/self.ts +++ b/packages/worker/src/api/controllers/global/self.ts @@ -111,8 +111,7 @@ export async function getSelf(ctx: UserCtx) { 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) }