Merge branch 'type-portal-user-store-2' of github.com:Budibase/budibase into type-portal-user-store-2
This commit is contained in:
commit
e2ecfc871d
|
@ -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
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"bulma": "^0.9.3",
|
||||
"next": "14.2.15",
|
||||
"next": "14.2.21",
|
||||
"node-fetch": "^3.2.10",
|
||||
"sass": "^1.52.3",
|
||||
"react": "17.0.2",
|
||||
|
|
|
@ -46,10 +46,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
|
||||
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
|
||||
|
||||
"@next/env@14.2.15":
|
||||
version "14.2.15"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.15.tgz#06d984e37e670d93ddd6790af1844aeb935f332f"
|
||||
integrity sha512-S1qaj25Wru2dUpcIZMjxeMVSwkt8BK4dmWHHiBuRstcIyOsMapqT4A4jSB6onvqeygkSSmOkyny9VVx8JIGamQ==
|
||||
"@next/env@14.2.21":
|
||||
version "14.2.21"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.21.tgz#09ff0813d29c596397e141205d4f5fd5c236bdd0"
|
||||
integrity sha512-lXcwcJd5oR01tggjWJ6SrNNYFGuOOMB9c251wUNkjCpkoXOPkDeF/15c3mnVlBqrW4JJXb2kVxDFhC4GduJt2A==
|
||||
|
||||
"@next/eslint-plugin-next@12.1.0":
|
||||
version "12.1.0"
|
||||
|
@ -58,50 +58,50 @@
|
|||
dependencies:
|
||||
glob "7.1.7"
|
||||
|
||||
"@next/swc-darwin-arm64@14.2.15":
|
||||
version "14.2.15"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.15.tgz#6386d585f39a1c490c60b72b1f76612ba4434347"
|
||||
integrity sha512-Rvh7KU9hOUBnZ9TJ28n2Oa7dD9cvDBKua9IKx7cfQQ0GoYUwg9ig31O2oMwH3wm+pE3IkAQ67ZobPfEgurPZIA==
|
||||
"@next/swc-darwin-arm64@14.2.21":
|
||||
version "14.2.21"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.21.tgz#32a31992aace1440981df9cf7cb3af7845d94fec"
|
||||
integrity sha512-HwEjcKsXtvszXz5q5Z7wCtrHeTTDSTgAbocz45PHMUjU3fBYInfvhR+ZhavDRUYLonm53aHZbB09QtJVJj8T7g==
|
||||
|
||||
"@next/swc-darwin-x64@14.2.15":
|
||||
version "14.2.15"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.15.tgz#b7baeedc6a28f7545ad2bc55adbab25f7b45cb89"
|
||||
integrity sha512-5TGyjFcf8ampZP3e+FyCax5zFVHi+Oe7sZyaKOngsqyaNEpOgkKB3sqmymkZfowy3ufGA/tUgDPPxpQx931lHg==
|
||||
"@next/swc-darwin-x64@14.2.21":
|
||||
version "14.2.21"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.21.tgz#5ab4b3f6685b6b52f810d0f5cf6e471480ddffdb"
|
||||
integrity sha512-TSAA2ROgNzm4FhKbTbyJOBrsREOMVdDIltZ6aZiKvCi/v0UwFmwigBGeqXDA97TFMpR3LNNpw52CbVelkoQBxA==
|
||||
|
||||
"@next/swc-linux-arm64-gnu@14.2.15":
|
||||
version "14.2.15"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.15.tgz#fa13c59d3222f70fb4cb3544ac750db2c6e34d02"
|
||||
integrity sha512-3Bwv4oc08ONiQ3FiOLKT72Q+ndEMyLNsc/D3qnLMbtUYTQAmkx9E/JRu0DBpHxNddBmNT5hxz1mYBphJ3mfrrw==
|
||||
"@next/swc-linux-arm64-gnu@14.2.21":
|
||||
version "14.2.21"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.21.tgz#8a0e1fa887aef19ca218af2af515d0a5ee67ba3f"
|
||||
integrity sha512-0Dqjn0pEUz3JG+AImpnMMW/m8hRtl1GQCNbO66V1yp6RswSTiKmnHf3pTX6xMdJYSemf3O4Q9ykiL0jymu0TuA==
|
||||
|
||||
"@next/swc-linux-arm64-musl@14.2.15":
|
||||
version "14.2.15"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.15.tgz#30e45b71831d9a6d6d18d7ac7d611a8d646a17f9"
|
||||
integrity sha512-k5xf/tg1FBv/M4CMd8S+JL3uV9BnnRmoe7F+GWC3DxkTCD9aewFRH1s5rJ1zkzDa+Do4zyN8qD0N8c84Hu96FQ==
|
||||
"@next/swc-linux-arm64-musl@14.2.21":
|
||||
version "14.2.21"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.21.tgz#ddad844406b42fa8965fe11250abc85c1fe0fd05"
|
||||
integrity sha512-Ggfw5qnMXldscVntwnjfaQs5GbBbjioV4B4loP+bjqNEb42fzZlAaK+ldL0jm2CTJga9LynBMhekNfV8W4+HBw==
|
||||
|
||||
"@next/swc-linux-x64-gnu@14.2.15":
|
||||
version "14.2.15"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.15.tgz#5065db17fc86f935ad117483f21f812dc1b39254"
|
||||
integrity sha512-kE6q38hbrRbKEkkVn62reLXhThLRh6/TvgSP56GkFNhU22TbIrQDEMrO7j0IcQHcew2wfykq8lZyHFabz0oBrA==
|
||||
"@next/swc-linux-x64-gnu@14.2.21":
|
||||
version "14.2.21"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.21.tgz#db55fd666f9ba27718f65caa54b622a912cdd16b"
|
||||
integrity sha512-uokj0lubN1WoSa5KKdThVPRffGyiWlm/vCc/cMkWOQHw69Qt0X1o3b2PyLLx8ANqlefILZh1EdfLRz9gVpG6tg==
|
||||
|
||||
"@next/swc-linux-x64-musl@14.2.15":
|
||||
version "14.2.15"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.15.tgz#3c4a4568d8be7373a820f7576cf33388b5dab47e"
|
||||
integrity sha512-PZ5YE9ouy/IdO7QVJeIcyLn/Rc4ml9M2G4y3kCM9MNf1YKvFY4heg3pVa/jQbMro+tP6yc4G2o9LjAz1zxD7tQ==
|
||||
"@next/swc-linux-x64-musl@14.2.21":
|
||||
version "14.2.21"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.21.tgz#dddb850353624efcd58c4c4e30ad8a1aab379642"
|
||||
integrity sha512-iAEBPzWNbciah4+0yI4s7Pce6BIoxTQ0AGCkxn/UBuzJFkYyJt71MadYQkjPqCQCJAFQ26sYh7MOKdU+VQFgPg==
|
||||
|
||||
"@next/swc-win32-arm64-msvc@14.2.15":
|
||||
version "14.2.15"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.15.tgz#fb812cc4ca0042868e32a6a021da91943bb08b98"
|
||||
integrity sha512-2raR16703kBvYEQD9HNLyb0/394yfqzmIeyp2nDzcPV4yPjqNUG3ohX6jX00WryXz6s1FXpVhsCo3i+g4RUX+g==
|
||||
"@next/swc-win32-arm64-msvc@14.2.21":
|
||||
version "14.2.21"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.21.tgz#290012ee57b196d3d2d04853e6bf0179cae9fbaf"
|
||||
integrity sha512-plykgB3vL2hB4Z32W3ktsfqyuyGAPxqwiyrAi2Mr8LlEUhNn9VgkiAl5hODSBpzIfWweX3er1f5uNpGDygfQVQ==
|
||||
|
||||
"@next/swc-win32-ia32-msvc@14.2.15":
|
||||
version "14.2.15"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.15.tgz#ec26e6169354f8ced240c1427be7fd485c5df898"
|
||||
integrity sha512-fyTE8cklgkyR1p03kJa5zXEaZ9El+kDNM5A+66+8evQS5e/6v0Gk28LqA0Jet8gKSOyP+OTm/tJHzMlGdQerdQ==
|
||||
"@next/swc-win32-ia32-msvc@14.2.21":
|
||||
version "14.2.21"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.21.tgz#c959135a78cab18cca588d11d1e33bcf199590d4"
|
||||
integrity sha512-w5bacz4Vxqrh06BjWgua3Yf7EMDb8iMcVhNrNx8KnJXt8t+Uu0Zg4JHLDL/T7DkTCEEfKXO/Er1fcfWxn2xfPA==
|
||||
|
||||
"@next/swc-win32-x64-msvc@14.2.15":
|
||||
version "14.2.15"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.15.tgz#18d68697002b282006771f8d92d79ade9efd35c4"
|
||||
integrity sha512-SzqGbsLsP9OwKNUG9nekShTwhj6JSB9ZLMWQ8g1gG6hdE5gQLncbnbymrwy2yVmH9nikSLYRYxYMFu78Ggp7/g==
|
||||
"@next/swc-win32-x64-msvc@14.2.21":
|
||||
version "14.2.21"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.21.tgz#21ff892286555b90538a7d1b505ea21a005d6ead"
|
||||
integrity sha512-sT6+llIkzpsexGYZq8cjjthRyRGe5cJVhqh12FmlbxHqna6zsDDK8UNaV7g41T6atFHCJUPeLb3uyAwrBwy0NA==
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
|
@ -1253,12 +1253,12 @@ natural-compare@^1.4.0:
|
|||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
|
||||
|
||||
next@14.2.15:
|
||||
version "14.2.15"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-14.2.15.tgz#348e5603e22649775d19c785c09a89c9acb5189a"
|
||||
integrity sha512-h9ctmOokpoDphRvMGnwOJAedT6zKhwqyZML9mDtspgf4Rh3Pn7UTYKqePNoDvhsWBAO5GoPNYshnAUGIazVGmw==
|
||||
next@14.2.21:
|
||||
version "14.2.21"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-14.2.21.tgz#f6da9e2abba1a0e4ca7a5273825daf06632554ba"
|
||||
integrity sha512-rZmLwucLHr3/zfDMYbJXbw0ZeoBpirxkXuvsJbk7UPorvPYZhP7vq7aHbKnU7dQNCYIimRrbB2pp3xmf+wsYUg==
|
||||
dependencies:
|
||||
"@next/env" "14.2.15"
|
||||
"@next/env" "14.2.21"
|
||||
"@swc/helpers" "0.5.5"
|
||||
busboy "1.6.0"
|
||||
caniuse-lite "^1.0.30001579"
|
||||
|
@ -1266,15 +1266,15 @@ next@14.2.15:
|
|||
postcss "8.4.31"
|
||||
styled-jsx "5.1.1"
|
||||
optionalDependencies:
|
||||
"@next/swc-darwin-arm64" "14.2.15"
|
||||
"@next/swc-darwin-x64" "14.2.15"
|
||||
"@next/swc-linux-arm64-gnu" "14.2.15"
|
||||
"@next/swc-linux-arm64-musl" "14.2.15"
|
||||
"@next/swc-linux-x64-gnu" "14.2.15"
|
||||
"@next/swc-linux-x64-musl" "14.2.15"
|
||||
"@next/swc-win32-arm64-msvc" "14.2.15"
|
||||
"@next/swc-win32-ia32-msvc" "14.2.15"
|
||||
"@next/swc-win32-x64-msvc" "14.2.15"
|
||||
"@next/swc-darwin-arm64" "14.2.21"
|
||||
"@next/swc-darwin-x64" "14.2.21"
|
||||
"@next/swc-linux-arm64-gnu" "14.2.21"
|
||||
"@next/swc-linux-arm64-musl" "14.2.21"
|
||||
"@next/swc-linux-x64-gnu" "14.2.21"
|
||||
"@next/swc-linux-x64-musl" "14.2.21"
|
||||
"@next/swc-win32-arm64-msvc" "14.2.21"
|
||||
"@next/swc-win32-ia32-msvc" "14.2.21"
|
||||
"@next/swc-win32-x64-msvc" "14.2.21"
|
||||
|
||||
node-domexception@^1.0.0:
|
||||
version "1.0.0"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||
"version": "3.2.29",
|
||||
"version": "3.2.39",
|
||||
"npmClient": "yarn",
|
||||
"concurrency": 20,
|
||||
"command": {
|
||||
|
|
|
@ -385,17 +385,17 @@ export function getCurrentContext(): ContextMap | undefined {
|
|||
}
|
||||
}
|
||||
|
||||
export function getFeatureFlags<T extends Record<string, any>>(
|
||||
export function getFeatureFlags(
|
||||
key: string
|
||||
): T | undefined {
|
||||
): Record<string, boolean> | undefined {
|
||||
const context = getCurrentContext()
|
||||
if (!context) {
|
||||
return undefined
|
||||
}
|
||||
return context.featureFlagCache?.[key] as T
|
||||
return context.featureFlagCache?.[key]
|
||||
}
|
||||
|
||||
export function setFeatureFlags(key: string, value: Record<string, any>) {
|
||||
export function setFeatureFlags(key: string, value: Record<string, boolean>) {
|
||||
const context = getCurrentContext()
|
||||
if (!context) {
|
||||
return
|
||||
|
|
|
@ -20,7 +20,7 @@ export type ContextMap = {
|
|||
clients: Record<string, GoogleSpreadsheet>
|
||||
}
|
||||
featureFlagCache?: {
|
||||
[key: string]: Record<string, any>
|
||||
[key: string]: Record<string, boolean>
|
||||
}
|
||||
viewToTableCache?: Record<string, Table>
|
||||
}
|
||||
|
|
|
@ -2,9 +2,10 @@ import env from "../environment"
|
|||
import * as crypto from "crypto"
|
||||
import * as context from "../context"
|
||||
import { PostHog, PostHogOptions } from "posthog-node"
|
||||
import { FeatureFlag } from "@budibase/types"
|
||||
import tracer from "dd-trace"
|
||||
import { Duration } from "../utils"
|
||||
import { cloneDeep } from "lodash"
|
||||
import { FeatureFlagDefaults } from "@budibase/types"
|
||||
|
||||
let posthog: PostHog | undefined
|
||||
export function init(opts?: PostHogOptions) {
|
||||
|
@ -30,74 +31,6 @@ export function shutdown() {
|
|||
posthog?.shutdown()
|
||||
}
|
||||
|
||||
export abstract class Flag<T> {
|
||||
static boolean(defaultValue: boolean): Flag<boolean> {
|
||||
return new BooleanFlag(defaultValue)
|
||||
}
|
||||
|
||||
static string(defaultValue: string): Flag<string> {
|
||||
return new StringFlag(defaultValue)
|
||||
}
|
||||
|
||||
static number(defaultValue: number): Flag<number> {
|
||||
return new NumberFlag(defaultValue)
|
||||
}
|
||||
|
||||
protected constructor(public defaultValue: T) {}
|
||||
|
||||
abstract parse(value: any): T
|
||||
}
|
||||
|
||||
type UnwrapFlag<F> = F extends Flag<infer U> ? U : never
|
||||
|
||||
export type FlagValues<T> = {
|
||||
[K in keyof T]: UnwrapFlag<T[K]>
|
||||
}
|
||||
|
||||
type KeysOfType<T, U> = {
|
||||
[K in keyof T]: T[K] extends Flag<U> ? K : never
|
||||
}[keyof T]
|
||||
|
||||
class BooleanFlag extends Flag<boolean> {
|
||||
parse(value: any) {
|
||||
if (typeof value === "string") {
|
||||
return ["true", "t", "1"].includes(value.toLowerCase())
|
||||
}
|
||||
|
||||
if (typeof value === "boolean") {
|
||||
return value
|
||||
}
|
||||
|
||||
throw new Error(`could not parse value "${value}" as boolean`)
|
||||
}
|
||||
}
|
||||
|
||||
class StringFlag extends Flag<string> {
|
||||
parse(value: any) {
|
||||
if (typeof value === "string") {
|
||||
return value
|
||||
}
|
||||
throw new Error(`could not parse value "${value}" as string`)
|
||||
}
|
||||
}
|
||||
|
||||
class NumberFlag extends Flag<number> {
|
||||
parse(value: any) {
|
||||
if (typeof value === "number") {
|
||||
return value
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
const parsed = parseFloat(value)
|
||||
if (!isNaN(parsed)) {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`could not parse value "${value}" as number`)
|
||||
}
|
||||
}
|
||||
|
||||
export interface EnvFlagEntry {
|
||||
tenantId: string
|
||||
key: string
|
||||
|
@ -120,7 +53,7 @@ export function parseEnvFlags(flags: string): EnvFlagEntry[] {
|
|||
return result
|
||||
}
|
||||
|
||||
export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
||||
export class FlagSet<T extends { [name: string]: boolean }> {
|
||||
// This is used to safely cache flags sets in the current request context.
|
||||
// Because multiple sets could theoretically exist, we don't want the cache of
|
||||
// one to leak into another.
|
||||
|
@ -130,34 +63,25 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
|||
this.setId = crypto.randomUUID()
|
||||
}
|
||||
|
||||
defaults(): FlagValues<T> {
|
||||
return Object.keys(this.flagSchema).reduce((acc, key) => {
|
||||
const typedKey = key as keyof T
|
||||
acc[typedKey] = this.flagSchema[key].defaultValue
|
||||
return acc
|
||||
}, {} as FlagValues<T>)
|
||||
defaults(): T {
|
||||
return cloneDeep(this.flagSchema)
|
||||
}
|
||||
|
||||
isFlagName(name: string | number | symbol): name is keyof T {
|
||||
return this.flagSchema[name as keyof T] !== undefined
|
||||
}
|
||||
|
||||
async get<K extends keyof T>(key: K): Promise<FlagValues<T>[K]> {
|
||||
async isEnabled<K extends keyof T>(key: K): Promise<T[K]> {
|
||||
const flags = await this.fetch()
|
||||
return flags[key]
|
||||
}
|
||||
|
||||
async isEnabled<K extends KeysOfType<T, boolean>>(key: K): Promise<boolean> {
|
||||
const flags = await this.fetch()
|
||||
return flags[key]
|
||||
}
|
||||
|
||||
async fetch(): Promise<FlagValues<T>> {
|
||||
async fetch(): Promise<T> {
|
||||
return await tracer.trace("features.fetch", async span => {
|
||||
const cachedFlags = context.getFeatureFlags<FlagValues<T>>(this.setId)
|
||||
const cachedFlags = context.getFeatureFlags(this.setId)
|
||||
if (cachedFlags) {
|
||||
span?.addTags({ fromCache: true })
|
||||
return cachedFlags
|
||||
return cachedFlags as T
|
||||
}
|
||||
|
||||
const tags: Record<string, any> = {}
|
||||
|
@ -189,7 +113,7 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
|||
|
||||
// @ts-expect-error - TS does not like you writing into a generic type,
|
||||
// but we know that it's okay in this case because it's just an object.
|
||||
flagValues[key as keyof FlagValues] = value
|
||||
flagValues[key as keyof T] = value
|
||||
tags[`flags.${key}.source`] = "environment"
|
||||
}
|
||||
|
||||
|
@ -217,11 +141,11 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
|||
tags[`readFromPostHog`] = true
|
||||
|
||||
const personProperties: Record<string, string> = { tenantId }
|
||||
const posthogFlags = await posthog.getAllFlagsAndPayloads(userId, {
|
||||
const posthogFlags = await posthog.getAllFlags(userId, {
|
||||
personProperties,
|
||||
})
|
||||
|
||||
for (const [name, value] of Object.entries(posthogFlags.featureFlags)) {
|
||||
for (const [name, value] of Object.entries(posthogFlags)) {
|
||||
if (!this.isFlagName(name)) {
|
||||
// We don't want an unexpected PostHog flag to break the app, so we
|
||||
// just log it and continue.
|
||||
|
@ -229,19 +153,20 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
|||
continue
|
||||
}
|
||||
|
||||
if (typeof value !== "boolean") {
|
||||
console.warn(`Invalid value for posthog flag "${name}": ${value}`)
|
||||
continue
|
||||
}
|
||||
|
||||
if (flagValues[name] === true || specificallySetFalse.has(name)) {
|
||||
// If the flag is already set to through environment variables, we
|
||||
// don't want to override it back to false here.
|
||||
continue
|
||||
}
|
||||
|
||||
const payload = posthogFlags.featureFlagPayloads?.[name]
|
||||
const flag = this.flagSchema[name]
|
||||
try {
|
||||
// @ts-expect-error - TS does not like you writing into a generic
|
||||
// type, but we know that it's okay in this case because it's just
|
||||
// an object.
|
||||
flagValues[name] = flag.parse(payload || value)
|
||||
// @ts-expect-error - TS does not like you writing into a generic type.
|
||||
flagValues[name] = value
|
||||
tags[`flags.${name}.source`] = "posthog"
|
||||
} catch (err) {
|
||||
// We don't want an invalid PostHog flag to break the app, so we just
|
||||
|
@ -262,18 +187,12 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
|||
}
|
||||
}
|
||||
|
||||
// This is the primary source of truth for feature flags. If you want to add a
|
||||
// new flag, add it here and use the `fetch` and `get` functions to access it.
|
||||
// All of the machinery in this file is to make sure that flags have their
|
||||
// default values set correctly and their types flow through the system.
|
||||
const flagsConfig: Record<FeatureFlag, Flag<any>> = {
|
||||
[FeatureFlag.DEFAULT_VALUES]: Flag.boolean(true),
|
||||
[FeatureFlag.AUTOMATION_BRANCHING]: Flag.boolean(true),
|
||||
[FeatureFlag.AI_CUSTOM_CONFIGS]: Flag.boolean(true),
|
||||
[FeatureFlag.BUDIBASE_AI]: Flag.boolean(true),
|
||||
[FeatureFlag.USE_ZOD_VALIDATOR]: Flag.boolean(env.isDev()),
|
||||
}
|
||||
export const flags = new FlagSet(flagsConfig)
|
||||
export const flags = new FlagSet(FeatureFlagDefaults)
|
||||
|
||||
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T
|
||||
export type FeatureFlags = UnwrapPromise<ReturnType<typeof flags.fetch>>
|
||||
export async function isEnabled(flag: keyof typeof FeatureFlagDefaults) {
|
||||
return await flags.isEnabled(flag)
|
||||
}
|
||||
|
||||
export async function all() {
|
||||
return await flags.fetch()
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { IdentityContext, IdentityType } from "@budibase/types"
|
||||
import { Flag, FlagSet, FlagValues, init, shutdown } from "../"
|
||||
import { FlagSet, init, shutdown } from "../"
|
||||
import * as context from "../../context"
|
||||
import environment, { withEnv } from "../../environment"
|
||||
import nodeFetch from "node-fetch"
|
||||
|
@ -7,10 +7,8 @@ import nock from "nock"
|
|||
import * as crypto from "crypto"
|
||||
|
||||
const schema = {
|
||||
TEST_BOOLEAN: Flag.boolean(false),
|
||||
TEST_STRING: Flag.string("default value"),
|
||||
TEST_NUMBER: Flag.number(0),
|
||||
TEST_BOOLEAN_DEFAULT_TRUE: Flag.boolean(true),
|
||||
TEST_BOOLEAN: false,
|
||||
TEST_BOOLEAN_DEFAULT_TRUE: true,
|
||||
}
|
||||
const flags = new FlagSet(schema)
|
||||
|
||||
|
@ -19,7 +17,7 @@ interface TestCase {
|
|||
identity?: Partial<IdentityContext>
|
||||
environmentFlags?: string
|
||||
posthogFlags?: PostHogFlags
|
||||
expected?: Partial<FlagValues<typeof schema>>
|
||||
expected?: Partial<typeof schema>
|
||||
errorMessage?: string | RegExp
|
||||
}
|
||||
|
||||
|
@ -83,22 +81,6 @@ describe("feature flags", () => {
|
|||
},
|
||||
expected: { TEST_BOOLEAN: true },
|
||||
},
|
||||
{
|
||||
it: "should be able to read string flags from PostHog",
|
||||
posthogFlags: {
|
||||
featureFlags: { TEST_STRING: true },
|
||||
featureFlagPayloads: { TEST_STRING: "test" },
|
||||
},
|
||||
expected: { TEST_STRING: "test" },
|
||||
},
|
||||
{
|
||||
it: "should be able to read numeric flags from PostHog",
|
||||
posthogFlags: {
|
||||
featureFlags: { TEST_NUMBER: true },
|
||||
featureFlagPayloads: { TEST_NUMBER: "123" },
|
||||
},
|
||||
expected: { TEST_NUMBER: 123 },
|
||||
},
|
||||
{
|
||||
it: "should not be able to override a negative environment flag from PostHog",
|
||||
environmentFlags: "default:!TEST_BOOLEAN",
|
||||
|
@ -177,7 +159,7 @@ describe("feature flags", () => {
|
|||
expect(values).toMatchObject(expected)
|
||||
|
||||
for (const [key, expectedValue] of Object.entries(expected)) {
|
||||
const value = await flags.get(key as keyof typeof schema)
|
||||
const value = await flags.isEnabled(key as keyof typeof schema)
|
||||
expect(value).toBe(expectedValue)
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { FeatureFlags, parseEnvFlags } from ".."
|
||||
import { FeatureFlags } from "@budibase/types"
|
||||
import { setEnv } from "../../environment"
|
||||
import { parseEnvFlags } from "../features"
|
||||
|
||||
function getCurrentFlags(): Record<string, Record<string, boolean>> {
|
||||
const result: Record<string, Record<string, boolean>> = {}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
declare module "./helpers" {
|
||||
export const cloneDeep: <T>(obj: T) => T
|
||||
}
|
|
@ -43,7 +43,6 @@
|
|||
export let showDataProviders = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const arrayTypes = ["attachment", "array"]
|
||||
|
||||
let anchorRight, dropdownRight
|
||||
let drawer
|
||||
|
@ -116,8 +115,11 @@
|
|||
}
|
||||
})
|
||||
$: fields = bindings
|
||||
.filter(x => arrayTypes.includes(x.fieldSchema?.type))
|
||||
.filter(x => x.fieldSchema?.tableId != null)
|
||||
.filter(
|
||||
x =>
|
||||
x.fieldSchema?.type === "attachment" ||
|
||||
(x.fieldSchema?.type === "array" && x.tableId)
|
||||
)
|
||||
.map(binding => {
|
||||
const { providerId, readableBinding, runtimeBinding } = binding
|
||||
const { name, type, tableId } = binding.fieldSchema
|
||||
|
|
|
@ -87,6 +87,7 @@
|
|||
let popover
|
||||
let user, tenantOwner
|
||||
let loaded = false
|
||||
let userFieldsToUpdate = {}
|
||||
|
||||
$: internalGroups = $groups?.filter(g => !g?.scimInfo?.isSync)
|
||||
|
||||
|
@ -164,40 +165,45 @@
|
|||
return label
|
||||
}
|
||||
|
||||
async function updateUserFirstName(evt) {
|
||||
async function saveUser() {
|
||||
try {
|
||||
await users.save({ ...user, firstName: evt.target.value })
|
||||
await users.save({ ...user, ...userFieldsToUpdate })
|
||||
userFieldsToUpdate = {}
|
||||
await fetchUser()
|
||||
} catch (error) {
|
||||
notifications.error("Error updating user")
|
||||
}
|
||||
}
|
||||
|
||||
async function updateUserFirstName(evt) {
|
||||
userFieldsToUpdate.firstName = evt.target.value
|
||||
}
|
||||
|
||||
async function updateUserLastName(evt) {
|
||||
try {
|
||||
await users.save({ ...user, lastName: evt.target.value })
|
||||
await fetchUser()
|
||||
} catch (error) {
|
||||
notifications.error("Error updating user")
|
||||
}
|
||||
userFieldsToUpdate.lastName = evt.target.value
|
||||
}
|
||||
|
||||
async function updateUserRole({ detail }) {
|
||||
let flags = {}
|
||||
if (detail === Constants.BudibaseRoles.Developer) {
|
||||
toggleFlags({ admin: { global: false }, builder: { global: true } })
|
||||
flags = { admin: { global: false }, builder: { global: true } }
|
||||
} else if (detail === Constants.BudibaseRoles.Admin) {
|
||||
toggleFlags({ admin: { global: true }, builder: { global: true } })
|
||||
flags = { admin: { global: true }, builder: { global: true } }
|
||||
} else if (detail === Constants.BudibaseRoles.AppUser) {
|
||||
toggleFlags({ admin: { global: false }, builder: { global: false } })
|
||||
flags = { admin: { global: false }, builder: { global: false } }
|
||||
} else if (detail === Constants.BudibaseRoles.Creator) {
|
||||
toggleFlags({
|
||||
flags = {
|
||||
admin: { global: false },
|
||||
builder: {
|
||||
global: false,
|
||||
creator: true,
|
||||
apps: user?.builder?.apps || [],
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
userFieldsToUpdate = {
|
||||
...userFieldsToUpdate,
|
||||
...flags,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -209,15 +215,6 @@
|
|||
tenantOwner = await users.getAccountHolder()
|
||||
}
|
||||
|
||||
async function toggleFlags(detail) {
|
||||
try {
|
||||
await users.save({ ...user, ...detail })
|
||||
await fetchUser()
|
||||
} catch (error) {
|
||||
notifications.error("Error updating user")
|
||||
}
|
||||
}
|
||||
|
||||
const addGroup = async groupId => {
|
||||
await groups.addUser(groupId, userId)
|
||||
await fetchUser()
|
||||
|
@ -296,7 +293,7 @@
|
|||
<Input
|
||||
disabled={readonly}
|
||||
value={user?.firstName}
|
||||
on:blur={updateUserFirstName}
|
||||
on:input={updateUserFirstName}
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
|
@ -304,7 +301,7 @@
|
|||
<Input
|
||||
disabled={readonly}
|
||||
value={user?.lastName}
|
||||
on:blur={updateUserLastName}
|
||||
on:input={updateUserLastName}
|
||||
/>
|
||||
</div>
|
||||
<!-- don't let a user remove the privileges that let them be here -->
|
||||
|
@ -325,6 +322,13 @@
|
|||
{/if}
|
||||
</div>
|
||||
</Layout>
|
||||
<div>
|
||||
<Button
|
||||
cta
|
||||
disabled={Object.keys(userFieldsToUpdate).length === 0}
|
||||
on:click={saveUser}>Save</Button
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if $licensing.groupsEnabled}
|
||||
<!-- User groups -->
|
||||
|
|
|
@ -291,8 +291,8 @@ const automationActions = (store: AutomationStore) => ({
|
|||
let result: (AutomationStep | AutomationTrigger)[] = []
|
||||
pathWay.forEach(path => {
|
||||
const { stepIdx, branchIdx } = path
|
||||
let last = result ? result[result.length - 1] : []
|
||||
if (!result) {
|
||||
let last = result.length ? result[result.length - 1] : []
|
||||
if (!result.length) {
|
||||
// Preceeding steps.
|
||||
result = steps.slice(0, stepIdx + 1)
|
||||
return
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
import { writable } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
|
||||
export function createFlagsStore() {
|
||||
const { subscribe, set } = writable({})
|
||||
|
||||
const actions = {
|
||||
fetch: async () => {
|
||||
const flags = await API.getFlags()
|
||||
set(flags)
|
||||
},
|
||||
updateFlag: async (flag, value) => {
|
||||
await API.updateFlag(flag, value)
|
||||
await actions.fetch()
|
||||
},
|
||||
toggleUiFeature: async feature => {
|
||||
await API.toggleUiFeature(feature)
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
...actions,
|
||||
}
|
||||
}
|
||||
|
||||
export const flags = createFlagsStore()
|
|
@ -0,0 +1,40 @@
|
|||
import { API } from "@/api"
|
||||
import { GetUserFlagsResponse } from "@budibase/types"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
|
||||
interface FlagsState {
|
||||
flags: GetUserFlagsResponse
|
||||
}
|
||||
|
||||
const INITIAL_FLAGS_STATE: FlagsState = {
|
||||
flags: {},
|
||||
}
|
||||
|
||||
export class FlagsStore extends BudiStore<FlagsState> {
|
||||
constructor() {
|
||||
super(INITIAL_FLAGS_STATE)
|
||||
|
||||
this.fetch = this.fetch.bind(this)
|
||||
this.updateFlag = this.updateFlag.bind(this)
|
||||
this.toggleUiFeature = this.toggleUiFeature.bind(this)
|
||||
}
|
||||
|
||||
async fetch() {
|
||||
const flags = await API.getFlags()
|
||||
this.update(state => ({
|
||||
...state,
|
||||
flags,
|
||||
}))
|
||||
}
|
||||
|
||||
async updateFlag(flag: string, value: any) {
|
||||
await API.updateFlag(flag, value)
|
||||
await this.fetch()
|
||||
}
|
||||
|
||||
async toggleUiFeature(feature: string) {
|
||||
await API.toggleUiFeature(feature)
|
||||
}
|
||||
}
|
||||
|
||||
export const flags = new FlagsStore()
|
|
@ -2,19 +2,24 @@ import { get } from "svelte/store"
|
|||
import { BudiStore } from "../BudiStore"
|
||||
import { previewStore } from "@/stores/builder"
|
||||
|
||||
interface BuilderHoverStore {
|
||||
hoverTimeout?: NodeJS.Timeout
|
||||
componentId: string | null
|
||||
}
|
||||
|
||||
export const INITIAL_HOVER_STATE = {
|
||||
componentId: null,
|
||||
}
|
||||
|
||||
export class HoverStore extends BudiStore {
|
||||
hoverTimeout
|
||||
export class HoverStore extends BudiStore<BuilderHoverStore> {
|
||||
hoverTimeout?: NodeJS.Timeout
|
||||
|
||||
constructor() {
|
||||
super({ ...INITIAL_HOVER_STATE })
|
||||
this.hover = this.hover.bind(this)
|
||||
}
|
||||
|
||||
hover(componentId, notifyClient = true) {
|
||||
hover(componentId: string, notifyClient = true) {
|
||||
clearTimeout(this.hoverTimeout)
|
||||
if (componentId) {
|
||||
this.processHover(componentId, notifyClient)
|
||||
|
@ -25,7 +30,7 @@ export class HoverStore extends BudiStore {
|
|||
}
|
||||
}
|
||||
|
||||
processHover(componentId, notifyClient) {
|
||||
processHover(componentId: string, notifyClient?: boolean) {
|
||||
if (componentId === get(this.store).componentId) {
|
||||
return
|
||||
}
|
|
@ -2,13 +2,19 @@ import { derived, get } from "svelte/store"
|
|||
import { componentStore } from "@/stores/builder"
|
||||
import { API } from "@/api"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
import { Layout } from "@budibase/types"
|
||||
|
||||
export const INITIAL_LAYOUT_STATE = {
|
||||
interface LayoutState {
|
||||
layouts: Layout[]
|
||||
selectedLayoutId: string | null
|
||||
}
|
||||
|
||||
export const INITIAL_LAYOUT_STATE: LayoutState = {
|
||||
layouts: [],
|
||||
selectedLayoutId: null,
|
||||
}
|
||||
|
||||
export class LayoutStore extends BudiStore {
|
||||
export class LayoutStore extends BudiStore<LayoutState> {
|
||||
constructor() {
|
||||
super(INITIAL_LAYOUT_STATE)
|
||||
|
||||
|
@ -22,14 +28,14 @@ export class LayoutStore extends BudiStore {
|
|||
this.store.set({ ...INITIAL_LAYOUT_STATE })
|
||||
}
|
||||
|
||||
syncAppLayouts(pkg) {
|
||||
syncAppLayouts(pkg: { layouts: Layout[] }) {
|
||||
this.update(state => ({
|
||||
...state,
|
||||
layouts: [...pkg.layouts],
|
||||
}))
|
||||
}
|
||||
|
||||
select(layoutId) {
|
||||
select(layoutId: string) {
|
||||
// Check this layout exists
|
||||
const state = get(this.store)
|
||||
const componentState = get(componentStore)
|
||||
|
@ -48,15 +54,15 @@ export class LayoutStore extends BudiStore {
|
|||
|
||||
// Select new layout
|
||||
this.update(state => {
|
||||
state.selectedLayoutId = layout._id
|
||||
state.selectedLayoutId = layout._id!
|
||||
return state
|
||||
})
|
||||
|
||||
componentStore.select(layout.props?._id)
|
||||
}
|
||||
|
||||
async deleteLayout(layout) {
|
||||
if (!layout?._id) {
|
||||
async deleteLayout(layout: Layout) {
|
||||
if (!layout?._id || !layout?._rev) {
|
||||
return
|
||||
}
|
||||
await API.deleteLayout(layout._id, layout._rev)
|
|
@ -2,27 +2,22 @@ import { get } from "svelte/store"
|
|||
import { API } from "@/api"
|
||||
import { appStore } from "@/stores/builder"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
import { AppNavigation, AppNavigationLink, UIObject } from "@budibase/types"
|
||||
|
||||
interface BuilderNavigationStore extends AppNavigation {}
|
||||
|
||||
export const INITIAL_NAVIGATION_STATE = {
|
||||
navigation: "Top",
|
||||
links: [],
|
||||
title: null,
|
||||
sticky: null,
|
||||
hideLogo: null,
|
||||
logoUrl: null,
|
||||
hideTitle: null,
|
||||
textAlign: "Left",
|
||||
navBackground: null,
|
||||
navWidth: null,
|
||||
navTextColor: null,
|
||||
}
|
||||
|
||||
export class NavigationStore extends BudiStore {
|
||||
export class NavigationStore extends BudiStore<BuilderNavigationStore> {
|
||||
constructor() {
|
||||
super(INITIAL_NAVIGATION_STATE)
|
||||
}
|
||||
|
||||
syncAppNavigation(nav) {
|
||||
syncAppNavigation(nav: AppNavigation) {
|
||||
this.update(state => ({
|
||||
...state,
|
||||
...nav,
|
||||
|
@ -33,15 +28,17 @@ export class NavigationStore extends BudiStore {
|
|||
this.store.set({ ...INITIAL_NAVIGATION_STATE })
|
||||
}
|
||||
|
||||
async save(navigation) {
|
||||
async save(navigation: AppNavigation) {
|
||||
const appId = get(appStore).appId
|
||||
const app = await API.saveAppMetadata(appId, { navigation })
|
||||
if (app.navigation) {
|
||||
this.syncAppNavigation(app.navigation)
|
||||
}
|
||||
}
|
||||
|
||||
async saveLink(url, title, roleId) {
|
||||
async saveLink(url: string, title: string, roleId: string) {
|
||||
const navigation = get(this.store)
|
||||
let links = [...(navigation?.links ?? [])]
|
||||
let links: AppNavigationLink[] = [...(navigation?.links ?? [])]
|
||||
|
||||
// Skip if we have an identical link
|
||||
if (links.find(link => link.url === url && link.text === title)) {
|
||||
|
@ -60,7 +57,7 @@ export class NavigationStore extends BudiStore {
|
|||
})
|
||||
}
|
||||
|
||||
async deleteLink(urls) {
|
||||
async deleteLink(urls: string[] | string) {
|
||||
const navigation = get(this.store)
|
||||
let links = navigation?.links
|
||||
if (!links?.length) {
|
||||
|
@ -86,7 +83,7 @@ export class NavigationStore extends BudiStore {
|
|||
})
|
||||
}
|
||||
|
||||
syncMetadata(metadata) {
|
||||
syncMetadata(metadata: UIObject) {
|
||||
const { navigation } = metadata
|
||||
this.syncAppNavigation(navigation)
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
|
||||
const INITIAL_PREVIEW_STATE = {
|
||||
previewDevice: "desktop",
|
||||
previewEventHandler: null,
|
||||
showPreview: false,
|
||||
selectedComponentContext: null,
|
||||
}
|
||||
|
||||
export const createPreviewStore = () => {
|
||||
const store = writable({
|
||||
...INITIAL_PREVIEW_STATE,
|
||||
})
|
||||
|
||||
const setDevice = device => {
|
||||
store.update(state => {
|
||||
state.previewDevice = device
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
// Potential evt names "eject-block", "dragging-new-component"
|
||||
const sendEvent = (name, payload) => {
|
||||
const { previewEventHandler } = get(store)
|
||||
previewEventHandler?.(name, payload)
|
||||
}
|
||||
|
||||
const registerEventHandler = handler => {
|
||||
store.update(state => {
|
||||
state.previewEventHandler = handler
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
const startDrag = component => {
|
||||
sendEvent("dragging-new-component", {
|
||||
dragging: true,
|
||||
component,
|
||||
})
|
||||
}
|
||||
|
||||
const stopDrag = () => {
|
||||
sendEvent("dragging-new-component", {
|
||||
dragging: false,
|
||||
})
|
||||
}
|
||||
|
||||
//load preview?
|
||||
const showPreview = isVisible => {
|
||||
store.update(state => {
|
||||
state.showPreview = isVisible
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
const setSelectedComponentContext = context => {
|
||||
store.update(state => {
|
||||
state.selectedComponentContext = context
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
const requestComponentContext = () => {
|
||||
sendEvent("request-context")
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
setDevice,
|
||||
sendEvent, //may not be required
|
||||
registerEventHandler,
|
||||
startDrag,
|
||||
stopDrag,
|
||||
showPreview,
|
||||
setSelectedComponentContext,
|
||||
requestComponentContext,
|
||||
}
|
||||
}
|
||||
|
||||
export const previewStore = createPreviewStore()
|
|
@ -0,0 +1,90 @@
|
|||
import { get } from "svelte/store"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
|
||||
type PreviewDevice = "desktop" | "tablet" | "mobile"
|
||||
type PreviewEventHandler = (name: string, payload?: any) => void
|
||||
type ComponentContext = Record<string, any>
|
||||
|
||||
interface PreviewState {
|
||||
previewDevice: PreviewDevice
|
||||
previewEventHandler: PreviewEventHandler | null
|
||||
showPreview: boolean
|
||||
selectedComponentContext: ComponentContext | null
|
||||
}
|
||||
|
||||
const INITIAL_PREVIEW_STATE: PreviewState = {
|
||||
previewDevice: "desktop",
|
||||
previewEventHandler: null,
|
||||
showPreview: false,
|
||||
selectedComponentContext: null,
|
||||
}
|
||||
|
||||
export class PreviewStore extends BudiStore<PreviewState> {
|
||||
constructor() {
|
||||
super(INITIAL_PREVIEW_STATE)
|
||||
|
||||
this.setDevice = this.setDevice.bind(this)
|
||||
this.sendEvent = this.sendEvent.bind(this)
|
||||
this.registerEventHandler = this.registerEventHandler.bind(this)
|
||||
this.startDrag = this.startDrag.bind(this)
|
||||
this.stopDrag = this.stopDrag.bind(this)
|
||||
this.showPreview = this.showPreview.bind(this)
|
||||
this.setSelectedComponentContext =
|
||||
this.setSelectedComponentContext.bind(this)
|
||||
this.requestComponentContext = this.requestComponentContext.bind(this)
|
||||
}
|
||||
|
||||
setDevice(device: PreviewDevice) {
|
||||
this.update(state => ({
|
||||
...state,
|
||||
previewDevice: device,
|
||||
}))
|
||||
}
|
||||
|
||||
// Potential evt names "eject-block", "dragging-new-component"
|
||||
sendEvent(name: string, payload?: any) {
|
||||
const { previewEventHandler } = get(this.store)
|
||||
previewEventHandler?.(name, payload)
|
||||
}
|
||||
|
||||
registerEventHandler(handler: PreviewEventHandler) {
|
||||
this.update(state => ({
|
||||
...state,
|
||||
previewEventHandler: handler,
|
||||
}))
|
||||
}
|
||||
|
||||
startDrag(component: any) {
|
||||
this.sendEvent("dragging-new-component", {
|
||||
dragging: true,
|
||||
component,
|
||||
})
|
||||
}
|
||||
|
||||
stopDrag() {
|
||||
this.sendEvent("dragging-new-component", {
|
||||
dragging: false,
|
||||
})
|
||||
}
|
||||
|
||||
//load preview?
|
||||
showPreview(isVisible: boolean) {
|
||||
this.update(state => ({
|
||||
...state,
|
||||
showPreview: isVisible,
|
||||
}))
|
||||
}
|
||||
|
||||
setSelectedComponentContext(context: ComponentContext) {
|
||||
this.update(state => ({
|
||||
...state,
|
||||
selectedComponentContext: context,
|
||||
}))
|
||||
}
|
||||
|
||||
requestComponentContext() {
|
||||
this.sendEvent("request-context")
|
||||
}
|
||||
}
|
||||
|
||||
export const previewStore = new PreviewStore()
|
|
@ -1,13 +1,16 @@
|
|||
import { appStore } from "./app"
|
||||
import { appsStore } from "@/stores/portal/apps"
|
||||
import { deploymentStore } from "./deployments"
|
||||
import { derived } from "svelte/store"
|
||||
import { derived, type Readable } from "svelte/store"
|
||||
import { DeploymentProgressResponse, DeploymentStatus } from "@budibase/types"
|
||||
|
||||
export const appPublished = derived(
|
||||
export const appPublished: Readable<boolean> = derived(
|
||||
[appStore, appsStore, deploymentStore],
|
||||
([$appStore, $appsStore, $deploymentStore]) => {
|
||||
const app = $appsStore.apps.find(app => app.devId === $appStore.appId)
|
||||
const deployments = $deploymentStore.filter(x => x.status === "SUCCESS")
|
||||
const deployments = $deploymentStore.filter(
|
||||
(x: DeploymentProgressResponse) => x.status === DeploymentStatus.SUCCESS
|
||||
)
|
||||
return app?.status === "published" && deployments.length > 0
|
||||
}
|
||||
)
|
|
@ -1,130 +0,0 @@
|
|||
import { writable, get, derived } from "svelte/store"
|
||||
import { datasources } from "./datasources"
|
||||
import { integrations } from "./integrations"
|
||||
import { API } from "@/api"
|
||||
import { duplicateName } from "@/helpers/duplicate"
|
||||
|
||||
const sortQueries = queryList => {
|
||||
queryList.sort((q1, q2) => {
|
||||
return q1.name.localeCompare(q2.name)
|
||||
})
|
||||
}
|
||||
|
||||
export function createQueriesStore() {
|
||||
const store = writable({
|
||||
list: [],
|
||||
selectedQueryId: null,
|
||||
})
|
||||
const derivedStore = derived(store, $store => ({
|
||||
...$store,
|
||||
selected: $store.list?.find(q => q._id === $store.selectedQueryId),
|
||||
}))
|
||||
|
||||
const fetch = async () => {
|
||||
const queries = await API.getQueries()
|
||||
sortQueries(queries)
|
||||
store.update(state => ({
|
||||
...state,
|
||||
list: queries,
|
||||
}))
|
||||
}
|
||||
|
||||
const save = async (datasourceId, query) => {
|
||||
const _integrations = get(integrations)
|
||||
const dataSource = get(datasources).list.filter(
|
||||
ds => ds._id === datasourceId
|
||||
)
|
||||
// Check if readable attribute is found
|
||||
if (dataSource.length !== 0) {
|
||||
const integration = _integrations[dataSource[0].source]
|
||||
const readable = integration.query[query.queryVerb].readable
|
||||
if (readable) {
|
||||
query.readable = readable
|
||||
}
|
||||
}
|
||||
query.datasourceId = datasourceId
|
||||
const savedQuery = await API.saveQuery(query)
|
||||
store.update(state => {
|
||||
const idx = state.list.findIndex(query => query._id === savedQuery._id)
|
||||
const queries = state.list
|
||||
if (idx >= 0) {
|
||||
queries.splice(idx, 1, savedQuery)
|
||||
} else {
|
||||
queries.push(savedQuery)
|
||||
}
|
||||
sortQueries(queries)
|
||||
return {
|
||||
list: queries,
|
||||
selectedQueryId: savedQuery._id,
|
||||
}
|
||||
})
|
||||
return savedQuery
|
||||
}
|
||||
|
||||
const importQueries = async ({ data, datasourceId }) => {
|
||||
return await API.importQueries(datasourceId, data)
|
||||
}
|
||||
|
||||
const select = id => {
|
||||
store.update(state => ({
|
||||
...state,
|
||||
selectedQueryId: id,
|
||||
}))
|
||||
}
|
||||
|
||||
const preview = async query => {
|
||||
const result = await API.previewQuery(query)
|
||||
// Assume all the fields are strings and create a basic schema from the
|
||||
// unique fields returned by the server
|
||||
const schema = {}
|
||||
for (let [field, metadata] of Object.entries(result.schema)) {
|
||||
schema[field] = metadata || { type: "string" }
|
||||
}
|
||||
return { ...result, schema, rows: result.rows || [] }
|
||||
}
|
||||
|
||||
const deleteQuery = async query => {
|
||||
await API.deleteQuery(query._id, query._rev)
|
||||
store.update(state => {
|
||||
state.list = state.list.filter(existing => existing._id !== query._id)
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
const duplicate = async query => {
|
||||
let list = get(store).list
|
||||
const newQuery = { ...query }
|
||||
const datasourceId = query.datasourceId
|
||||
|
||||
delete newQuery._id
|
||||
delete newQuery._rev
|
||||
newQuery.name = duplicateName(
|
||||
query.name,
|
||||
list.map(q => q.name)
|
||||
)
|
||||
|
||||
return await save(datasourceId, newQuery)
|
||||
}
|
||||
|
||||
const removeDatasourceQueries = datasourceId => {
|
||||
store.update(state => ({
|
||||
...state,
|
||||
list: state.list.filter(table => table.datasourceId !== datasourceId),
|
||||
}))
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: derivedStore.subscribe,
|
||||
fetch,
|
||||
init: fetch,
|
||||
select,
|
||||
save,
|
||||
import: importQueries,
|
||||
delete: deleteQuery,
|
||||
preview,
|
||||
duplicate,
|
||||
removeDatasourceQueries,
|
||||
}
|
||||
}
|
||||
|
||||
export const queries = createQueriesStore()
|
|
@ -0,0 +1,156 @@
|
|||
import { derived, get, Writable } from "svelte/store"
|
||||
import { datasources } from "./datasources"
|
||||
import { integrations } from "./integrations"
|
||||
import { API } from "@/api"
|
||||
import { duplicateName } from "@/helpers/duplicate"
|
||||
import { DerivedBudiStore } from "@/stores/BudiStore"
|
||||
import {
|
||||
Query,
|
||||
QueryPreview,
|
||||
PreviewQueryResponse,
|
||||
SaveQueryRequest,
|
||||
ImportRestQueryRequest,
|
||||
QuerySchema,
|
||||
} from "@budibase/types"
|
||||
|
||||
const sortQueries = (queryList: Query[]) => {
|
||||
queryList.sort((q1, q2) => {
|
||||
return q1.name.localeCompare(q2.name)
|
||||
})
|
||||
}
|
||||
|
||||
interface BuilderQueryStore {
|
||||
list: Query[]
|
||||
selectedQueryId: string | null
|
||||
}
|
||||
|
||||
interface DerivedQueryStore extends BuilderQueryStore {
|
||||
selected?: Query
|
||||
}
|
||||
|
||||
export class QueryStore extends DerivedBudiStore<
|
||||
BuilderQueryStore,
|
||||
DerivedQueryStore
|
||||
> {
|
||||
constructor() {
|
||||
const makeDerivedStore = (store: Writable<BuilderQueryStore>) => {
|
||||
return derived(store, ($store): DerivedQueryStore => {
|
||||
return {
|
||||
list: $store.list,
|
||||
selectedQueryId: $store.selectedQueryId,
|
||||
selected: $store.list?.find(q => q._id === $store.selectedQueryId),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
super(
|
||||
{
|
||||
list: [],
|
||||
selectedQueryId: null,
|
||||
},
|
||||
makeDerivedStore
|
||||
)
|
||||
|
||||
this.select = this.select.bind(this)
|
||||
}
|
||||
|
||||
async fetch() {
|
||||
const queries = await API.getQueries()
|
||||
sortQueries(queries)
|
||||
this.store.update(state => ({
|
||||
...state,
|
||||
list: queries,
|
||||
}))
|
||||
}
|
||||
|
||||
async save(datasourceId: string, query: SaveQueryRequest) {
|
||||
const _integrations = get(integrations)
|
||||
const dataSource = get(datasources).list.filter(
|
||||
ds => ds._id === datasourceId
|
||||
)
|
||||
// Check if readable attribute is found
|
||||
if (dataSource.length !== 0) {
|
||||
const integration = _integrations[dataSource[0].source]
|
||||
const readable = integration.query[query.queryVerb].readable
|
||||
if (readable) {
|
||||
query.readable = readable
|
||||
}
|
||||
}
|
||||
query.datasourceId = datasourceId
|
||||
const savedQuery = await API.saveQuery(query)
|
||||
this.store.update(state => {
|
||||
const idx = state.list.findIndex(query => query._id === savedQuery._id)
|
||||
const queries = state.list
|
||||
if (idx >= 0) {
|
||||
queries.splice(idx, 1, savedQuery)
|
||||
} else {
|
||||
queries.push(savedQuery)
|
||||
}
|
||||
sortQueries(queries)
|
||||
return {
|
||||
list: queries,
|
||||
selectedQueryId: savedQuery._id || null,
|
||||
}
|
||||
})
|
||||
return savedQuery
|
||||
}
|
||||
|
||||
async importQueries(data: ImportRestQueryRequest) {
|
||||
return await API.importQueries(data)
|
||||
}
|
||||
|
||||
select(id: string | null) {
|
||||
this.store.update(state => ({
|
||||
...state,
|
||||
selectedQueryId: id,
|
||||
}))
|
||||
}
|
||||
|
||||
async preview(query: QueryPreview): Promise<PreviewQueryResponse> {
|
||||
const result = await API.previewQuery(query)
|
||||
// Assume all the fields are strings and create a basic schema from the
|
||||
// unique fields returned by the server
|
||||
const schema: Record<string, QuerySchema> = {}
|
||||
for (let [field, metadata] of Object.entries(result.schema)) {
|
||||
schema[field] = (metadata as QuerySchema) || { type: "string" }
|
||||
}
|
||||
return { ...result, schema, rows: result.rows || [] }
|
||||
}
|
||||
|
||||
async delete(query: Query) {
|
||||
if (!query._id || !query._rev) {
|
||||
throw new Error("Query ID or Revision is missing")
|
||||
}
|
||||
await API.deleteQuery(query._id, query._rev)
|
||||
this.store.update(state => ({
|
||||
...state,
|
||||
list: state.list.filter(existing => existing._id !== query._id),
|
||||
}))
|
||||
}
|
||||
|
||||
async duplicate(query: Query) {
|
||||
let list = get(this.store).list
|
||||
const newQuery = { ...query }
|
||||
const datasourceId = query.datasourceId
|
||||
|
||||
delete newQuery._id
|
||||
delete newQuery._rev
|
||||
newQuery.name = duplicateName(
|
||||
query.name,
|
||||
list.map(q => q.name)
|
||||
)
|
||||
|
||||
return await this.save(datasourceId, newQuery)
|
||||
}
|
||||
|
||||
removeDatasourceQueries(datasourceId: string) {
|
||||
this.store.update(state => ({
|
||||
...state,
|
||||
list: state.list.filter(table => table.datasourceId !== datasourceId),
|
||||
}))
|
||||
}
|
||||
|
||||
init = this.fetch
|
||||
}
|
||||
|
||||
export const queries = new QueryStore()
|
|
@ -1,88 +0,0 @@
|
|||
import { derived, writable, get } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
|
||||
export function createRolesStore() {
|
||||
const store = writable([])
|
||||
const enriched = derived(store, $store => {
|
||||
return $store.map(role => ({
|
||||
...role,
|
||||
|
||||
// Ensure we have new metadata for all roles
|
||||
uiMetadata: {
|
||||
displayName: role.uiMetadata?.displayName || role.name,
|
||||
color:
|
||||
role.uiMetadata?.color || "var(--spectrum-global-color-magenta-400)",
|
||||
description: role.uiMetadata?.description || "Custom role",
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
function setRoles(roles) {
|
||||
store.set(
|
||||
roles.sort((a, b) => {
|
||||
const priorityA = RoleUtils.getRolePriority(a._id)
|
||||
const priorityB = RoleUtils.getRolePriority(b._id)
|
||||
if (priorityA !== priorityB) {
|
||||
return priorityA > priorityB ? -1 : 1
|
||||
}
|
||||
const nameA = a.uiMetadata?.displayName || a.name
|
||||
const nameB = b.uiMetadata?.displayName || b.name
|
||||
return nameA < nameB ? -1 : 1
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const actions = {
|
||||
fetch: async () => {
|
||||
const roles = await API.getRoles()
|
||||
setRoles(roles)
|
||||
},
|
||||
fetchByAppId: async appId => {
|
||||
const { roles } = await API.getRolesForApp(appId)
|
||||
setRoles(roles)
|
||||
},
|
||||
delete: async role => {
|
||||
await API.deleteRole(role._id, role._rev)
|
||||
await actions.fetch()
|
||||
},
|
||||
save: async role => {
|
||||
const savedRole = await API.saveRole(role)
|
||||
await actions.fetch()
|
||||
return savedRole
|
||||
},
|
||||
replace: (roleId, role) => {
|
||||
// Handles external updates of roles
|
||||
if (!roleId) {
|
||||
return
|
||||
}
|
||||
|
||||
// Handle deletion
|
||||
if (!role) {
|
||||
store.update(state => state.filter(x => x._id !== roleId))
|
||||
return
|
||||
}
|
||||
|
||||
// Add new role
|
||||
const index = get(store).findIndex(x => x._id === role._id)
|
||||
if (index === -1) {
|
||||
store.update(state => [...state, role])
|
||||
}
|
||||
|
||||
// Update existing role
|
||||
else if (role) {
|
||||
store.update(state => {
|
||||
state[index] = role
|
||||
return [...state]
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: enriched.subscribe,
|
||||
...actions,
|
||||
}
|
||||
}
|
||||
|
||||
export const roles = createRolesStore()
|
|
@ -0,0 +1,94 @@
|
|||
import { derived, get, type Writable } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
import { DerivedBudiStore } from "../BudiStore"
|
||||
import { Role } from "@budibase/types"
|
||||
|
||||
export class RoleStore extends DerivedBudiStore<Role[], Role[]> {
|
||||
constructor() {
|
||||
const makeDerivedStore = (store: Writable<Role[]>) =>
|
||||
derived(store, $store => {
|
||||
return $store.map((role: Role) => ({
|
||||
...role,
|
||||
// Ensure we have new metadata for all roles
|
||||
uiMetadata: {
|
||||
displayName: role.uiMetadata?.displayName || role.name,
|
||||
color:
|
||||
role.uiMetadata?.color ||
|
||||
"var(--spectrum-global-color-magenta-400)",
|
||||
description: role.uiMetadata?.description || "Custom role",
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
super([], makeDerivedStore)
|
||||
}
|
||||
|
||||
setRoles = (roles: Role[]) => {
|
||||
this.set(
|
||||
roles.sort((a, b) => {
|
||||
// Ensure we have valid IDs for priority comparison
|
||||
const priorityA = RoleUtils.getRolePriority(a._id)
|
||||
const priorityB = RoleUtils.getRolePriority(b._id)
|
||||
if (priorityA !== priorityB) {
|
||||
return priorityA > priorityB ? -1 : 1
|
||||
}
|
||||
const nameA = a.uiMetadata?.displayName || a.name
|
||||
const nameB = b.uiMetadata?.displayName || b.name
|
||||
return nameA < nameB ? -1 : 1
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
fetch = async () => {
|
||||
const roles = await API.getRoles()
|
||||
this.setRoles(roles)
|
||||
}
|
||||
|
||||
fetchByAppId = async (appId: string) => {
|
||||
const { roles } = await API.getRolesForApp(appId)
|
||||
this.setRoles(roles)
|
||||
}
|
||||
|
||||
delete = async (role: Role) => {
|
||||
if (!role._id || !role._rev) {
|
||||
return
|
||||
}
|
||||
await API.deleteRole(role._id, role._rev)
|
||||
await this.fetch()
|
||||
}
|
||||
|
||||
save = async (role: Role) => {
|
||||
const savedRole = await API.saveRole(role)
|
||||
await this.fetch()
|
||||
return savedRole
|
||||
}
|
||||
|
||||
replace = (roleId: string, role?: Role) => {
|
||||
// Handles external updates of roles
|
||||
if (!roleId) {
|
||||
return
|
||||
}
|
||||
|
||||
// Handle deletion
|
||||
if (!role) {
|
||||
this.update(state => state.filter(x => x._id !== roleId))
|
||||
return
|
||||
}
|
||||
|
||||
// Add new role
|
||||
const index = get(this).findIndex(x => x._id === role._id)
|
||||
if (index === -1) {
|
||||
this.update(state => [...state, role])
|
||||
}
|
||||
// Update existing role
|
||||
else if (role) {
|
||||
this.update(state => {
|
||||
state[index] = role
|
||||
return [...state]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const roles = new RoleStore()
|
|
@ -6,18 +6,29 @@ import { automationStore } from "./automations"
|
|||
import { API } from "@/api"
|
||||
import { getSequentialName } from "@/helpers/duplicate"
|
||||
|
||||
const initialState = {}
|
||||
interface RowAction {
|
||||
id: string
|
||||
name: string
|
||||
tableId: string
|
||||
allowedSources?: string[]
|
||||
}
|
||||
|
||||
export class RowActionStore extends BudiStore {
|
||||
interface RowActionState {
|
||||
[tableId: string]: RowAction[]
|
||||
}
|
||||
|
||||
const initialState: RowActionState = {}
|
||||
|
||||
export class RowActionStore extends BudiStore<RowActionState> {
|
||||
constructor() {
|
||||
super(initialState)
|
||||
}
|
||||
|
||||
reset = () => {
|
||||
this.store.set(initialState)
|
||||
this.set(initialState)
|
||||
}
|
||||
|
||||
refreshRowActions = async sourceId => {
|
||||
refreshRowActions = async (sourceId: string) => {
|
||||
if (!sourceId) {
|
||||
return
|
||||
}
|
||||
|
@ -34,26 +45,30 @@ export class RowActionStore extends BudiStore {
|
|||
|
||||
// Fetch row actions for this table
|
||||
const res = await API.rowActions.fetch(tableId)
|
||||
const actions = Object.values(res || {})
|
||||
const actions = Object.values(res || {}) as RowAction[]
|
||||
this.update(state => ({
|
||||
...state,
|
||||
[tableId]: actions,
|
||||
}))
|
||||
}
|
||||
|
||||
createRowAction = async (tableId, viewId, name) => {
|
||||
createRowAction = async (tableId: string, viewId?: string, name?: string) => {
|
||||
if (!tableId) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get a unique name for this action
|
||||
if (!name) {
|
||||
const existingRowActions = get(this.store)[tableId] || []
|
||||
const existingRowActions = get(this)[tableId] || []
|
||||
name = getSequentialName(existingRowActions, "New row action ", {
|
||||
getName: x => x.name,
|
||||
})
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
throw new Error("Failed to generate a unique name for the row action")
|
||||
}
|
||||
|
||||
// Create the action
|
||||
const res = await API.rowActions.create(tableId, name)
|
||||
|
||||
|
@ -73,41 +88,35 @@ export class RowActionStore extends BudiStore {
|
|||
return res
|
||||
}
|
||||
|
||||
enableView = async (tableId, rowActionId, viewId) => {
|
||||
enableView = async (tableId: string, rowActionId: string, viewId: string) => {
|
||||
await API.rowActions.enableView(tableId, rowActionId, viewId)
|
||||
await this.refreshRowActions(tableId)
|
||||
}
|
||||
|
||||
disableView = async (tableId, rowActionId, viewId) => {
|
||||
disableView = async (
|
||||
tableId: string,
|
||||
rowActionId: string,
|
||||
viewId: string
|
||||
) => {
|
||||
await API.rowActions.disableView(tableId, rowActionId, viewId)
|
||||
await this.refreshRowActions(tableId)
|
||||
}
|
||||
|
||||
rename = async (tableId, rowActionId, name) => {
|
||||
await API.rowActions.update({
|
||||
tableId,
|
||||
rowActionId,
|
||||
name,
|
||||
})
|
||||
await this.refreshRowActions(tableId)
|
||||
automationStore.actions.fetch()
|
||||
}
|
||||
|
||||
delete = async (tableId, rowActionId) => {
|
||||
delete = async (tableId: string, rowActionId: string) => {
|
||||
await API.rowActions.delete(tableId, rowActionId)
|
||||
await this.refreshRowActions(tableId)
|
||||
// We don't need to refresh automations as we can only delete row actions
|
||||
// from the automations store, so we already handle the state update there
|
||||
}
|
||||
|
||||
trigger = async (sourceId, rowActionId, rowId) => {
|
||||
trigger = async (sourceId: string, rowActionId: string, rowId: string) => {
|
||||
await API.rowActions.trigger(sourceId, rowActionId, rowId)
|
||||
}
|
||||
}
|
||||
|
||||
const store = new RowActionStore()
|
||||
const derivedStore = derived(store, $store => {
|
||||
let map = {}
|
||||
const derivedStore = derived<RowActionStore, RowActionState>(store, $store => {
|
||||
const map: RowActionState = {}
|
||||
|
||||
// Generate an entry for every view as well
|
||||
Object.keys($store || {}).forEach(tableId => {
|
||||
|
@ -115,7 +124,7 @@ const derivedStore = derived(store, $store => {
|
|||
map[tableId] = $store[tableId]
|
||||
for (let action of $store[tableId]) {
|
||||
const otherSources = (action.allowedSources || []).filter(
|
||||
sourceId => sourceId !== tableId
|
||||
(sourceId: string) => sourceId !== tableId
|
||||
)
|
||||
for (let source of otherSources) {
|
||||
map[source] ??= []
|
|
@ -1,35 +0,0 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
import { appStore } from "./app"
|
||||
|
||||
const createsnippets = () => {
|
||||
const store = writable([])
|
||||
|
||||
const syncMetadata = metadata => {
|
||||
store.set(metadata?.snippets || [])
|
||||
}
|
||||
|
||||
const saveSnippet = async updatedSnippet => {
|
||||
const snippets = [
|
||||
...get(store).filter(snippet => snippet.name !== updatedSnippet.name),
|
||||
updatedSnippet,
|
||||
]
|
||||
const app = await API.saveAppMetadata(get(appStore).appId, { snippets })
|
||||
syncMetadata(app)
|
||||
}
|
||||
|
||||
const deleteSnippet = async snippetName => {
|
||||
const snippets = get(store).filter(snippet => snippet.name !== snippetName)
|
||||
const app = await API.saveAppMetadata(get(appStore).appId, { snippets })
|
||||
syncMetadata(app)
|
||||
}
|
||||
|
||||
return {
|
||||
...store,
|
||||
syncMetadata,
|
||||
saveSnippet,
|
||||
deleteSnippet,
|
||||
}
|
||||
}
|
||||
|
||||
export const snippets = createsnippets()
|
|
@ -0,0 +1,32 @@
|
|||
import { get } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
import { appStore } from "./app"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
import { Snippet, UpdateAppResponse } from "@budibase/types"
|
||||
|
||||
export class SnippetStore extends BudiStore<Snippet[]> {
|
||||
constructor() {
|
||||
super([])
|
||||
}
|
||||
|
||||
syncMetadata = (metadata: UpdateAppResponse) => {
|
||||
this.set(metadata?.snippets || [])
|
||||
}
|
||||
|
||||
saveSnippet = async (updatedSnippet: Snippet) => {
|
||||
const snippets = [
|
||||
...get(this).filter(snippet => snippet.name !== updatedSnippet.name),
|
||||
updatedSnippet,
|
||||
]
|
||||
const app = await API.saveAppMetadata(get(appStore).appId, { snippets })
|
||||
this.syncMetadata(app)
|
||||
}
|
||||
|
||||
deleteSnippet = async (snippetName: string) => {
|
||||
const snippets = get(this).filter(snippet => snippet.name !== snippetName)
|
||||
const app = await API.saveAppMetadata(get(appStore).appId, { snippets })
|
||||
this.syncMetadata(app)
|
||||
}
|
||||
}
|
||||
|
||||
export const snippets = new SnippetStore()
|
|
@ -1,58 +0,0 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
import { ensureValidTheme, DefaultAppTheme } from "@budibase/shared-core"
|
||||
|
||||
export const createThemeStore = () => {
|
||||
const store = writable({
|
||||
theme: DefaultAppTheme,
|
||||
customTheme: {},
|
||||
})
|
||||
|
||||
const syncAppTheme = app => {
|
||||
store.update(state => {
|
||||
const theme = ensureValidTheme(app.theme, DefaultAppTheme)
|
||||
return {
|
||||
...state,
|
||||
theme,
|
||||
customTheme: app.customTheme,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const save = async (theme, appId) => {
|
||||
const app = await API.saveAppMetadata(appId, { theme })
|
||||
store.update(state => {
|
||||
state.theme = app.theme
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
const saveCustom = async (theme, appId) => {
|
||||
const updated = { ...get(store).customTheme, ...theme }
|
||||
const app = await API.saveAppMetadata(appId, { customTheme: updated })
|
||||
store.update(state => {
|
||||
state.customTheme = app.customTheme
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
const syncMetadata = metadata => {
|
||||
const { theme, customTheme } = metadata
|
||||
store.update(state => ({
|
||||
...state,
|
||||
theme: ensureValidTheme(theme, DefaultAppTheme),
|
||||
customTheme,
|
||||
}))
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
update: store.update,
|
||||
syncMetadata,
|
||||
syncAppTheme,
|
||||
save,
|
||||
saveCustom,
|
||||
}
|
||||
}
|
||||
|
||||
export const themeStore = createThemeStore()
|
|
@ -0,0 +1,58 @@
|
|||
import { get } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
import { ensureValidTheme, DefaultAppTheme } from "@budibase/shared-core"
|
||||
import { App, UpdateAppResponse, Theme, AppCustomTheme } from "@budibase/types"
|
||||
|
||||
interface ThemeState {
|
||||
theme: Theme
|
||||
customTheme: AppCustomTheme
|
||||
}
|
||||
|
||||
export class ThemeStore extends BudiStore<ThemeState> {
|
||||
constructor() {
|
||||
super({
|
||||
theme: DefaultAppTheme,
|
||||
customTheme: {},
|
||||
})
|
||||
}
|
||||
|
||||
syncAppTheme = (app: App) => {
|
||||
this.update(state => {
|
||||
const theme = ensureValidTheme(app.theme, DefaultAppTheme)
|
||||
return {
|
||||
...state,
|
||||
theme,
|
||||
customTheme: app.customTheme || {},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
save = async (theme: Theme, appId: string) => {
|
||||
const app = await API.saveAppMetadata(appId, { theme })
|
||||
this.update(state => ({
|
||||
...state,
|
||||
theme: ensureValidTheme(app.theme, DefaultAppTheme),
|
||||
}))
|
||||
}
|
||||
|
||||
saveCustom = async (theme: Partial<AppCustomTheme>, appId: string) => {
|
||||
const updated = { ...get(this).customTheme, ...theme }
|
||||
const app = await API.saveAppMetadata(appId, { customTheme: updated })
|
||||
this.update(state => ({
|
||||
...state,
|
||||
customTheme: app.customTheme || {},
|
||||
}))
|
||||
}
|
||||
|
||||
syncMetadata = (metadata: UpdateAppResponse) => {
|
||||
const { theme, customTheme } = metadata
|
||||
this.update(state => ({
|
||||
...state,
|
||||
theme: ensureValidTheme(theme, DefaultAppTheme),
|
||||
customTheme: customTheme || {},
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
export const themeStore = new ThemeStore()
|
|
@ -1,62 +0,0 @@
|
|||
import { writable, get, derived } from "svelte/store"
|
||||
|
||||
export const createUserStore = () => {
|
||||
const store = writable([])
|
||||
|
||||
const init = users => {
|
||||
store.set(users)
|
||||
}
|
||||
|
||||
const updateUser = user => {
|
||||
const $users = get(store)
|
||||
if (!$users.some(x => x.sessionId === user.sessionId)) {
|
||||
store.set([...$users, user])
|
||||
} else {
|
||||
store.update(state => {
|
||||
const index = state.findIndex(x => x.sessionId === user.sessionId)
|
||||
state[index] = user
|
||||
return state.slice()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const removeUser = sessionId => {
|
||||
store.update(state => {
|
||||
return state.filter(x => x.sessionId !== sessionId)
|
||||
})
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
store.set([])
|
||||
}
|
||||
|
||||
return {
|
||||
...store,
|
||||
actions: {
|
||||
init,
|
||||
updateUser,
|
||||
removeUser,
|
||||
reset,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const userStore = createUserStore()
|
||||
|
||||
export const userSelectedResourceMap = derived(userStore, $userStore => {
|
||||
let map = {}
|
||||
$userStore.forEach(user => {
|
||||
const resource = user.builderMetadata?.selectedResourceId
|
||||
if (resource) {
|
||||
if (!map[resource]) {
|
||||
map[resource] = []
|
||||
}
|
||||
map[resource].push(user)
|
||||
}
|
||||
})
|
||||
return map
|
||||
})
|
||||
|
||||
export const isOnlyUser = derived(userStore, $userStore => {
|
||||
return $userStore.length < 2
|
||||
})
|
|
@ -0,0 +1,59 @@
|
|||
import { get, derived } from "svelte/store"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
import { UIUser } from "@budibase/types"
|
||||
|
||||
export class UserStore extends BudiStore<UIUser[]> {
|
||||
constructor() {
|
||||
super([])
|
||||
}
|
||||
|
||||
init(users: UIUser[]) {
|
||||
this.set(users)
|
||||
}
|
||||
|
||||
updateUser(user: UIUser) {
|
||||
const $users = get(this)
|
||||
if (!$users.some(x => x.sessionId === user.sessionId)) {
|
||||
this.set([...$users, user])
|
||||
} else {
|
||||
this.update(state => {
|
||||
const index = state.findIndex(x => x.sessionId === user.sessionId)
|
||||
state[index] = user
|
||||
return state.slice()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
removeUser(sessionId: string) {
|
||||
this.update(state => {
|
||||
return state.filter(x => x.sessionId !== sessionId)
|
||||
})
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.set([])
|
||||
}
|
||||
}
|
||||
|
||||
export const userStore = new UserStore()
|
||||
|
||||
export const userSelectedResourceMap = derived(
|
||||
userStore,
|
||||
($userStore): Record<string, UIUser[]> => {
|
||||
let map: Record<string, UIUser[]> = {}
|
||||
$userStore.forEach(user => {
|
||||
const resource = user.builderMetadata?.selectedResourceId
|
||||
if (resource) {
|
||||
if (!map[resource]) {
|
||||
map[resource] = []
|
||||
}
|
||||
map[resource].push(user)
|
||||
}
|
||||
})
|
||||
return map
|
||||
}
|
||||
)
|
||||
|
||||
export const isOnlyUser = derived(userStore, $userStore => {
|
||||
return $userStore.length < 2
|
||||
})
|
|
@ -1,95 +0,0 @@
|
|||
import { createWebsocket } from "@budibase/frontend-core"
|
||||
import {
|
||||
automationStore,
|
||||
userStore,
|
||||
appStore,
|
||||
themeStore,
|
||||
navigationStore,
|
||||
deploymentStore,
|
||||
snippets,
|
||||
datasources,
|
||||
tables,
|
||||
roles,
|
||||
} from "@/stores/builder"
|
||||
import { get } from "svelte/store"
|
||||
import { auth, appsStore } from "@/stores/portal"
|
||||
import { screenStore } from "./screens"
|
||||
import { SocketEvent, BuilderSocketEvent, helpers } from "@budibase/shared-core"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
|
||||
export const createBuilderWebsocket = appId => {
|
||||
const socket = createWebsocket("/socket/builder")
|
||||
|
||||
// Built-in events
|
||||
socket.on("connect", () => {
|
||||
socket.emit(BuilderSocketEvent.SelectApp, { appId }, ({ users }) => {
|
||||
userStore.actions.init(users)
|
||||
})
|
||||
})
|
||||
socket.on("connect_error", err => {
|
||||
console.error("Failed to connect to builder websocket:", err.message)
|
||||
})
|
||||
socket.on("disconnect", () => {
|
||||
userStore.actions.reset()
|
||||
})
|
||||
|
||||
// User events
|
||||
socket.onOther(SocketEvent.UserUpdate, ({ user }) => {
|
||||
userStore.actions.updateUser(user)
|
||||
})
|
||||
socket.onOther(SocketEvent.UserDisconnect, ({ sessionId }) => {
|
||||
userStore.actions.removeUser(sessionId)
|
||||
})
|
||||
socket.onOther(BuilderSocketEvent.LockTransfer, ({ userId }) => {
|
||||
if (userId === get(auth)?.user?._id) {
|
||||
appStore.update(state => ({
|
||||
...state,
|
||||
hasLock: true,
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
// Data section events
|
||||
socket.onOther(BuilderSocketEvent.TableChange, ({ id, table }) => {
|
||||
tables.replaceTable(id, table)
|
||||
})
|
||||
socket.onOther(BuilderSocketEvent.DatasourceChange, ({ id, datasource }) => {
|
||||
datasources.replaceDatasource(id, datasource)
|
||||
})
|
||||
|
||||
// Role events
|
||||
socket.onOther(BuilderSocketEvent.RoleChange, ({ id, role }) => {
|
||||
roles.replace(id, role)
|
||||
})
|
||||
|
||||
// Design section events
|
||||
socket.onOther(BuilderSocketEvent.ScreenChange, ({ id, screen }) => {
|
||||
screenStore.replace(id, screen)
|
||||
})
|
||||
|
||||
// App events
|
||||
socket.onOther(BuilderSocketEvent.AppMetadataChange, ({ metadata }) => {
|
||||
appStore.syncMetadata(metadata)
|
||||
themeStore.syncMetadata(metadata)
|
||||
navigationStore.syncMetadata(metadata)
|
||||
snippets.syncMetadata(metadata)
|
||||
})
|
||||
socket.onOther(
|
||||
BuilderSocketEvent.AppPublishChange,
|
||||
async ({ user, published }) => {
|
||||
await appsStore.load()
|
||||
if (published) {
|
||||
await deploymentStore.load()
|
||||
}
|
||||
const verb = published ? "published" : "unpublished"
|
||||
notifications.success(`${helpers.getUserLabel(user)} ${verb} this app`)
|
||||
}
|
||||
)
|
||||
|
||||
// Automation events
|
||||
socket.onOther(BuilderSocketEvent.AutomationChange, ({ id, automation }) => {
|
||||
automationStore.actions.replace(id, automation)
|
||||
})
|
||||
|
||||
return socket
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
import { createWebsocket } from "@budibase/frontend-core"
|
||||
import {
|
||||
automationStore,
|
||||
userStore,
|
||||
appStore,
|
||||
themeStore,
|
||||
navigationStore,
|
||||
deploymentStore,
|
||||
snippets,
|
||||
datasources,
|
||||
tables,
|
||||
roles,
|
||||
} from "@/stores/builder"
|
||||
import { get } from "svelte/store"
|
||||
import { auth, appsStore } from "@/stores/portal"
|
||||
import { screenStore } from "./screens"
|
||||
import { SocketEvent, BuilderSocketEvent, helpers } from "@budibase/shared-core"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import { Automation, Datasource, Role, Table, UIUser } from "@budibase/types"
|
||||
|
||||
export const createBuilderWebsocket = (appId: string) => {
|
||||
const socket = createWebsocket("/socket/builder")
|
||||
|
||||
// Built-in events
|
||||
socket.on("connect", () => {
|
||||
socket.emit(
|
||||
BuilderSocketEvent.SelectApp,
|
||||
{ appId },
|
||||
({ users }: { users: UIUser[] }) => {
|
||||
userStore.init(users)
|
||||
}
|
||||
)
|
||||
})
|
||||
socket.on("connect_error", err => {
|
||||
console.error("Failed to connect to builder websocket:", err.message)
|
||||
})
|
||||
socket.on("disconnect", () => {
|
||||
userStore.reset()
|
||||
})
|
||||
|
||||
// User events
|
||||
socket.onOther(SocketEvent.UserUpdate, ({ user }: { user: UIUser }) => {
|
||||
userStore.updateUser(user)
|
||||
})
|
||||
socket.onOther(
|
||||
SocketEvent.UserDisconnect,
|
||||
({ sessionId }: { sessionId: string }) => {
|
||||
userStore.removeUser(sessionId)
|
||||
}
|
||||
)
|
||||
socket.onOther(
|
||||
BuilderSocketEvent.LockTransfer,
|
||||
({ userId }: { userId: string }) => {
|
||||
if (userId === get(auth)?.user?._id) {
|
||||
appStore.update(state => ({
|
||||
...state,
|
||||
hasLock: true,
|
||||
}))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Data section events
|
||||
socket.onOther(
|
||||
BuilderSocketEvent.TableChange,
|
||||
({ id, table }: { id: string; table: Table }) => {
|
||||
tables.replaceTable(id, table)
|
||||
}
|
||||
)
|
||||
socket.onOther(
|
||||
BuilderSocketEvent.DatasourceChange,
|
||||
({ id, datasource }: { id: string; datasource: Datasource }) => {
|
||||
datasources.replaceDatasource(id, datasource)
|
||||
}
|
||||
)
|
||||
|
||||
// Role events
|
||||
socket.onOther(
|
||||
BuilderSocketEvent.RoleChange,
|
||||
({ id, role }: { id: string; role: Role }) => {
|
||||
roles.replace(id, role)
|
||||
}
|
||||
)
|
||||
|
||||
// Design section events
|
||||
socket.onOther(
|
||||
BuilderSocketEvent.ScreenChange,
|
||||
({ id, screen }: { id: string; screen: Screen }) => {
|
||||
screenStore.replace(id, screen)
|
||||
}
|
||||
)
|
||||
|
||||
// App events
|
||||
socket.onOther(
|
||||
BuilderSocketEvent.AppMetadataChange,
|
||||
({ metadata }: { metadata: any }) => {
|
||||
appStore.syncMetadata(metadata)
|
||||
themeStore.syncMetadata(metadata)
|
||||
navigationStore.syncMetadata(metadata)
|
||||
snippets.syncMetadata(metadata)
|
||||
}
|
||||
)
|
||||
socket.onOther(
|
||||
BuilderSocketEvent.AppPublishChange,
|
||||
async ({ user, published }: { user: UIUser; published: boolean }) => {
|
||||
await appsStore.load()
|
||||
if (published) {
|
||||
await deploymentStore.load()
|
||||
}
|
||||
const verb = published ? "published" : "unpublished"
|
||||
notifications.success(`${helpers.getUserLabel(user)} ${verb} this app`)
|
||||
}
|
||||
)
|
||||
|
||||
// Automation events
|
||||
socket.onOther(
|
||||
BuilderSocketEvent.AutomationChange,
|
||||
({ id, automation }: { id: string; automation: Automation }) => {
|
||||
automationStore.actions.replace(id, automation)
|
||||
}
|
||||
)
|
||||
|
||||
return socket
|
||||
}
|
|
@ -1,19 +1,8 @@
|
|||
import { derived, Readable } from "svelte/store"
|
||||
import { auth } from "@/stores/portal"
|
||||
import { FeatureFlags, FeatureFlagDefaults } from "@budibase/types"
|
||||
|
||||
export const INITIAL_FEATUREFLAG_STATE = {
|
||||
SQS: false,
|
||||
DEFAULT_VALUES: false,
|
||||
BUDIBASE_AI: false,
|
||||
AI_CUSTOM_CONFIGS: false,
|
||||
}
|
||||
|
||||
export const featureFlags: Readable<Record<string, any>> = derived(
|
||||
[auth],
|
||||
([$auth]) => {
|
||||
return {
|
||||
...INITIAL_FEATUREFLAG_STATE,
|
||||
export const featureFlags: Readable<FeatureFlags> = derived(auth, $auth => ({
|
||||
...FeatureFlagDefaults,
|
||||
...($auth?.user?.flags || {}),
|
||||
}
|
||||
}
|
||||
)
|
||||
}))
|
||||
|
|
|
@ -47,9 +47,12 @@ class GroupStore extends BudiStore<UserGroup[]> {
|
|||
|
||||
async delete(group: UserGroup) {
|
||||
await API.deleteGroup(group._id!, group._rev!)
|
||||
this.update(state => {
|
||||
state = state.filter(state => state._id !== group._id)
|
||||
return state
|
||||
this.update(groups => {
|
||||
const index = groups.findIndex(g => g._id === group._id)
|
||||
if (index !== -1) {
|
||||
groups.splice(index, 1)
|
||||
}
|
||||
return groups
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -227,7 +227,7 @@ class LicensingStore extends BudiStore<LicensingState> {
|
|||
MonthlyQuotaName.AUTOMATIONS,
|
||||
].reduce((acc: MonthlyMetrics, key) => {
|
||||
const limit = license.quotas.usage.monthly[key].value
|
||||
const used = (usage.monthly.current?.[key] || 0 / limit) * 100
|
||||
const used = ((usage.monthly.current?.[key] || 0) / limit) * 100
|
||||
acc[key] = limit > -1 ? Math.floor(used) : -1
|
||||
return acc
|
||||
}, {})
|
||||
|
@ -236,7 +236,7 @@ class LicensingStore extends BudiStore<LicensingState> {
|
|||
const staticMetrics = [StaticQuotaName.APPS, StaticQuotaName.ROWS].reduce(
|
||||
(acc: StaticMetrics, key) => {
|
||||
const limit = license.quotas.usage.static[key].value
|
||||
const used = (usage.usageQuota[key] || 0 / limit) * 100
|
||||
const used = ((usage.usageQuota[key] || 0) / limit) * 100
|
||||
acc[key] = limit > -1 ? Math.floor(used) : -1
|
||||
return acc
|
||||
},
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import { derived, Readable } from "svelte/store"
|
||||
import { admin } from "./admin"
|
||||
import { auth } from "./auth"
|
||||
import { isEnabled } from "@/helpers/featureFlags"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
import { FeatureFlag } from "@budibase/types"
|
||||
|
||||
interface MenuItem {
|
||||
title: string
|
||||
|
@ -73,13 +71,11 @@ export const menu: Readable<MenuItem[]> = derived(
|
|||
title: "Environment",
|
||||
href: "/builder/portal/settings/environment",
|
||||
},
|
||||
]
|
||||
if (isEnabled(FeatureFlag.AI_CUSTOM_CONFIGS)) {
|
||||
settingsSubPages.push({
|
||||
{
|
||||
title: "AI",
|
||||
href: "/builder/portal/settings/ai",
|
||||
})
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
if (!cloud) {
|
||||
settingsSubPages.push({
|
||||
|
|
|
@ -11,14 +11,10 @@ class OIDCStore extends BudiStore<PublicOIDCConfig> {
|
|||
|
||||
async init() {
|
||||
const tenantId = get(auth).tenantId
|
||||
const config = await API.getOIDCConfig(tenantId)
|
||||
if (Object.keys(config || {}).length) {
|
||||
const configs = await API.getOIDCConfigs(tenantId)
|
||||
// Just use the first config for now.
|
||||
// We will be support multiple logins buttons later on.
|
||||
this.set(config[0])
|
||||
} else {
|
||||
this.set({})
|
||||
}
|
||||
this.set(configs[0] || {})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,6 +1,10 @@
|
|||
import { createAPIClient } from "@budibase/frontend-core"
|
||||
import { authStore } from "../stores/auth.js"
|
||||
import { notificationStore, devToolsEnabled, devToolsStore } from "../stores/"
|
||||
import { authStore } from "../stores/auth"
|
||||
import {
|
||||
notificationStore,
|
||||
devToolsEnabled,
|
||||
devToolsStore,
|
||||
} from "../stores/index"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
export const API = createAPIClient({
|
|
@ -1,5 +1,5 @@
|
|||
import { API } from "./api.js"
|
||||
import { patchAPI } from "./patches.js"
|
||||
import { API } from "./api"
|
||||
import { patchAPI } from "./patches"
|
||||
|
||||
// Certain endpoints which return rows need patched so that they transform
|
||||
// and enrich the row docs, so that they can be correctly handled by the
|
||||
|
|
|
@ -66,10 +66,9 @@ export const patchAPI = API => {
|
|||
}
|
||||
}
|
||||
const fetchRelationshipData = API.fetchRelationshipData
|
||||
API.fetchRelationshipData = async params => {
|
||||
const tableId = params?.tableId
|
||||
const rows = await fetchRelationshipData(params)
|
||||
return await enrichRows(rows, tableId)
|
||||
API.fetchRelationshipData = async (sourceId, rowId, fieldName) => {
|
||||
const rows = await fetchRelationshipData(sourceId, rowId, fieldName)
|
||||
return await enrichRows(rows, sourceId)
|
||||
}
|
||||
const fetchTableData = API.fetchTableData
|
||||
API.fetchTableData = async tableId => {
|
||||
|
@ -85,9 +84,9 @@ export const patchAPI = API => {
|
|||
}
|
||||
}
|
||||
const fetchViewData = API.fetchViewData
|
||||
API.fetchViewData = async params => {
|
||||
API.fetchViewData = async (viewName, params) => {
|
||||
const tableId = params?.tableId
|
||||
const rows = await fetchViewData(params)
|
||||
const rows = await fetchViewData(viewName, params)
|
||||
return await enrichRows(rows, tableId)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
interface Window {
|
||||
"##BUDIBASE_APP_ID##": string
|
||||
"##BUDIBASE_IN_BUILDER##": string
|
||||
MIGRATING_APP: boolean
|
||||
}
|
|
@ -29,7 +29,7 @@ import { ActionTypes } from "./constants"
|
|||
import {
|
||||
fetchDatasourceSchema,
|
||||
fetchDatasourceDefinition,
|
||||
} from "./utils/schema.js"
|
||||
} from "./utils/schema"
|
||||
import { getAPIKey } from "./utils/api.js"
|
||||
import { enrichButtonActions } from "./utils/buttonActions.js"
|
||||
import { processStringSync, makePropSafe } from "@budibase/string-templates"
|
||||
|
|
|
@ -2,7 +2,9 @@ import { API } from "api"
|
|||
import { writable } from "svelte/store"
|
||||
|
||||
const createAuthStore = () => {
|
||||
const store = writable(null)
|
||||
const store = writable<{
|
||||
csrfToken?: string
|
||||
} | null>(null)
|
||||
|
||||
// Fetches the user object if someone is logged in and has reloaded the page
|
||||
const fetchUser = async () => {
|
|
@ -1,7 +1,7 @@
|
|||
import { derived } from "svelte/store"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
import { devToolsStore } from "../devTools.js"
|
||||
import { authStore } from "../auth.js"
|
||||
import { authStore } from "../auth"
|
||||
import { devToolsEnabled } from "./devToolsEnabled.js"
|
||||
|
||||
// Derive the current role of the logged-in user
|
||||
|
|
|
@ -6,7 +6,7 @@ const DEFAULT_NOTIFICATION_TIMEOUT = 3000
|
|||
const createNotificationStore = () => {
|
||||
let block = false
|
||||
|
||||
const store = writable([])
|
||||
const store = writable<{ id: string; message: string; count: number }[]>([])
|
||||
|
||||
const blockNotifications = (timeout = 1000) => {
|
||||
block = true
|
||||
|
@ -14,11 +14,11 @@ const createNotificationStore = () => {
|
|||
}
|
||||
|
||||
const send = (
|
||||
message,
|
||||
message: string,
|
||||
type = "info",
|
||||
icon,
|
||||
icon: string,
|
||||
autoDismiss = true,
|
||||
duration,
|
||||
duration?: number,
|
||||
count = 1
|
||||
) => {
|
||||
if (block) {
|
||||
|
@ -66,7 +66,7 @@ const createNotificationStore = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const dismiss = id => {
|
||||
const dismiss = (id: string) => {
|
||||
store.update(state => {
|
||||
return state.filter(n => n.id !== id)
|
||||
})
|
||||
|
@ -76,13 +76,13 @@ const createNotificationStore = () => {
|
|||
subscribe: store.subscribe,
|
||||
actions: {
|
||||
send,
|
||||
info: (msg, autoDismiss, duration) =>
|
||||
info: (msg: string, autoDismiss?: boolean, duration?: number) =>
|
||||
send(msg, "info", "Info", autoDismiss ?? true, duration),
|
||||
success: (msg, autoDismiss, duration) =>
|
||||
success: (msg: string, autoDismiss?: boolean, duration?: number) =>
|
||||
send(msg, "success", "CheckmarkCircle", autoDismiss ?? true, duration),
|
||||
warning: (msg, autoDismiss, duration) =>
|
||||
warning: (msg: string, autoDismiss?: boolean, duration?: number) =>
|
||||
send(msg, "warning", "Alert", autoDismiss ?? true, duration),
|
||||
error: (msg, autoDismiss, duration) =>
|
||||
error: (msg: string, autoDismiss?: boolean, duration?: number) =>
|
||||
send(msg, "error", "Alert", autoDismiss ?? false, duration),
|
||||
blockNotifications,
|
||||
dismiss,
|
|
@ -4,8 +4,24 @@ import { API } from "api"
|
|||
import { peekStore } from "./peek"
|
||||
import { builderStore } from "./builder"
|
||||
|
||||
interface Route {
|
||||
path: string
|
||||
screenId: string
|
||||
}
|
||||
|
||||
interface StoreType {
|
||||
routes: Route[]
|
||||
routeParams: {}
|
||||
activeRoute?: Route | null
|
||||
routeSessionId: number
|
||||
routerLoaded: boolean
|
||||
queryParams?: {
|
||||
peek?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
const createRouteStore = () => {
|
||||
const initialState = {
|
||||
const initialState: StoreType = {
|
||||
routes: [],
|
||||
routeParams: {},
|
||||
activeRoute: null,
|
||||
|
@ -22,7 +38,7 @@ const createRouteStore = () => {
|
|||
} catch (error) {
|
||||
routeConfig = null
|
||||
}
|
||||
let routes = []
|
||||
const routes: Route[] = []
|
||||
Object.values(routeConfig?.routes || {}).forEach(route => {
|
||||
Object.entries(route.subpaths || {}).forEach(([path, config]) => {
|
||||
routes.push({
|
||||
|
@ -43,13 +59,13 @@ const createRouteStore = () => {
|
|||
return state
|
||||
})
|
||||
}
|
||||
const setRouteParams = routeParams => {
|
||||
const setRouteParams = (routeParams: StoreType["routeParams"]) => {
|
||||
store.update(state => {
|
||||
state.routeParams = routeParams
|
||||
return state
|
||||
})
|
||||
}
|
||||
const setQueryParams = queryParams => {
|
||||
const setQueryParams = (queryParams: { peek?: boolean }) => {
|
||||
store.update(state => {
|
||||
state.queryParams = {
|
||||
...queryParams,
|
||||
|
@ -60,13 +76,13 @@ const createRouteStore = () => {
|
|||
return state
|
||||
})
|
||||
}
|
||||
const setActiveRoute = route => {
|
||||
const setActiveRoute = (route: string) => {
|
||||
store.update(state => {
|
||||
state.activeRoute = state.routes.find(x => x.path === route)
|
||||
return state
|
||||
})
|
||||
}
|
||||
const navigate = (url, peek, externalNewTab) => {
|
||||
const navigate = (url: string, peek: boolean, externalNewTab: boolean) => {
|
||||
if (get(builderStore).inBuilder) {
|
||||
return
|
||||
}
|
||||
|
@ -93,7 +109,7 @@ const createRouteStore = () => {
|
|||
const setRouterLoaded = () => {
|
||||
store.update(state => ({ ...state, routerLoaded: true }))
|
||||
}
|
||||
const createFullURL = relativeURL => {
|
||||
const createFullURL = (relativeURL: string) => {
|
||||
if (!relativeURL?.startsWith("/")) {
|
||||
return relativeURL
|
||||
}
|
|
@ -216,11 +216,11 @@ const deleteRowHandler = async action => {
|
|||
const triggerAutomationHandler = async action => {
|
||||
const { fields, notificationOverride, timeout } = action.parameters
|
||||
try {
|
||||
const result = await API.triggerAutomation({
|
||||
automationId: action.parameters.automationId,
|
||||
const result = await API.triggerAutomation(
|
||||
action.parameters.automationId,
|
||||
fields,
|
||||
timeout,
|
||||
})
|
||||
timeout
|
||||
)
|
||||
|
||||
// Value will exist if automation is synchronous, so return it.
|
||||
if (result.value) {
|
||||
|
|
|
@ -1,13 +1,5 @@
|
|||
import { API } from "api"
|
||||
import TableFetch from "@budibase/frontend-core/src/fetch/TableFetch.js"
|
||||
import ViewFetch from "@budibase/frontend-core/src/fetch/ViewFetch.js"
|
||||
import QueryFetch from "@budibase/frontend-core/src/fetch/QueryFetch.js"
|
||||
import RelationshipFetch from "@budibase/frontend-core/src/fetch/RelationshipFetch.js"
|
||||
import NestedProviderFetch from "@budibase/frontend-core/src/fetch/NestedProviderFetch.js"
|
||||
import FieldFetch from "@budibase/frontend-core/src/fetch/FieldFetch.js"
|
||||
import JSONArrayFetch from "@budibase/frontend-core/src/fetch/JSONArrayFetch.js"
|
||||
import ViewV2Fetch from "@budibase/frontend-core/src/fetch/ViewV2Fetch.js"
|
||||
import QueryArrayFetch from "@budibase/frontend-core/src/fetch/QueryArrayFetch"
|
||||
import { DataFetchMap, DataFetchType } from "@budibase/frontend-core"
|
||||
|
||||
/**
|
||||
* Constructs a fetch instance for a given datasource.
|
||||
|
@ -16,22 +8,20 @@ import QueryArrayFetch from "@budibase/frontend-core/src/fetch/QueryArrayFetch"
|
|||
* @param datasource the datasource
|
||||
* @returns
|
||||
*/
|
||||
const getDatasourceFetchInstance = datasource => {
|
||||
const handler = {
|
||||
table: TableFetch,
|
||||
view: ViewFetch,
|
||||
viewV2: ViewV2Fetch,
|
||||
query: QueryFetch,
|
||||
link: RelationshipFetch,
|
||||
provider: NestedProviderFetch,
|
||||
field: FieldFetch,
|
||||
jsonarray: JSONArrayFetch,
|
||||
queryarray: QueryArrayFetch,
|
||||
}[datasource?.type]
|
||||
const getDatasourceFetchInstance = <
|
||||
TDatasource extends { type: DataFetchType }
|
||||
>(
|
||||
datasource: TDatasource
|
||||
) => {
|
||||
const handler = DataFetchMap[datasource?.type]
|
||||
if (!handler) {
|
||||
return null
|
||||
}
|
||||
return new handler({ API })
|
||||
return new handler({
|
||||
API,
|
||||
datasource: datasource as never,
|
||||
query: null as any,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -39,21 +29,23 @@ const getDatasourceFetchInstance = datasource => {
|
|||
* @param datasource the datasource to fetch the schema for
|
||||
* @param options options for enriching the schema
|
||||
*/
|
||||
export const fetchDatasourceSchema = async (
|
||||
datasource,
|
||||
export const fetchDatasourceSchema = async <
|
||||
TDatasource extends { type: DataFetchType }
|
||||
>(
|
||||
datasource: TDatasource,
|
||||
options = { enrichRelationships: false, formSchema: false }
|
||||
) => {
|
||||
const instance = getDatasourceFetchInstance(datasource)
|
||||
const definition = await instance?.getDefinition(datasource)
|
||||
if (!definition) {
|
||||
const definition = await instance?.getDefinition()
|
||||
if (!instance || !definition) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Get the normal schema as long as we aren't wanting a form schema
|
||||
let schema
|
||||
let schema: any
|
||||
if (datasource?.type !== "query" || !options?.formSchema) {
|
||||
schema = instance.getSchema(datasource, definition)
|
||||
} else if (definition.parameters?.length) {
|
||||
schema = instance.getSchema(definition as any)
|
||||
} else if ("parameters" in definition && definition.parameters?.length) {
|
||||
schema = {}
|
||||
definition.parameters.forEach(param => {
|
||||
schema[param.name] = { ...param, type: "string" }
|
||||
|
@ -73,7 +65,12 @@ export const fetchDatasourceSchema = async (
|
|||
}
|
||||
|
||||
// Enrich schema with relationships if required
|
||||
if (definition?.sql && options?.enrichRelationships) {
|
||||
if (
|
||||
definition &&
|
||||
"sql" in definition &&
|
||||
definition.sql &&
|
||||
options?.enrichRelationships
|
||||
) {
|
||||
const relationshipAdditions = await getRelationshipSchemaAdditions(schema)
|
||||
schema = {
|
||||
...schema,
|
||||
|
@ -89,20 +86,26 @@ export const fetchDatasourceSchema = async (
|
|||
* Fetches the definition of any kind of datasource.
|
||||
* @param datasource the datasource to fetch the schema for
|
||||
*/
|
||||
export const fetchDatasourceDefinition = async datasource => {
|
||||
export const fetchDatasourceDefinition = async <
|
||||
TDatasource extends { type: DataFetchType }
|
||||
>(
|
||||
datasource: TDatasource
|
||||
) => {
|
||||
const instance = getDatasourceFetchInstance(datasource)
|
||||
return await instance?.getDefinition(datasource)
|
||||
return await instance?.getDefinition()
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the schema of relationship fields for a SQL table schema
|
||||
* @param schema the schema to enrich
|
||||
*/
|
||||
export const getRelationshipSchemaAdditions = async schema => {
|
||||
export const getRelationshipSchemaAdditions = async (
|
||||
schema: Record<string, any>
|
||||
) => {
|
||||
if (!schema) {
|
||||
return null
|
||||
}
|
||||
let relationshipAdditions = {}
|
||||
let relationshipAdditions: Record<string, any> = {}
|
||||
for (let fieldKey of Object.keys(schema)) {
|
||||
const fieldSchema = schema[fieldKey]
|
||||
if (fieldSchema?.type === "link") {
|
||||
|
@ -110,7 +113,10 @@ export const getRelationshipSchemaAdditions = async schema => {
|
|||
type: "table",
|
||||
tableId: fieldSchema?.tableId,
|
||||
})
|
||||
Object.keys(linkSchema || {}).forEach(linkKey => {
|
||||
if (!linkSchema) {
|
||||
continue
|
||||
}
|
||||
Object.keys(linkSchema).forEach(linkKey => {
|
||||
relationshipAdditions[`${fieldKey}.${linkKey}`] = {
|
||||
type: linkSchema[linkKey].type,
|
||||
externalType: linkSchema[linkKey].externalType,
|
|
@ -16,7 +16,7 @@ import { BaseAPIClient } from "./types"
|
|||
export interface ConfigEndpoints {
|
||||
getConfig: (type: ConfigType) => Promise<FindConfigResponse>
|
||||
getTenantConfig: (tentantId: string) => Promise<GetPublicSettingsResponse>
|
||||
getOIDCConfig: (tenantId: string) => Promise<GetPublicOIDCConfigResponse>
|
||||
getOIDCConfigs: (tenantId: string) => Promise<GetPublicOIDCConfigResponse>
|
||||
getOIDCLogos: () => Promise<Config<OIDCLogosConfig>>
|
||||
saveConfig: (config: SaveConfigRequest) => Promise<SaveConfigResponse>
|
||||
deleteConfig: (id: string, rev: string) => Promise<DeleteConfigResponse>
|
||||
|
@ -73,7 +73,7 @@ export const buildConfigEndpoints = (API: BaseAPIClient): ConfigEndpoints => ({
|
|||
* Gets the OIDC config for a certain tenant.
|
||||
* @param tenantId the tenant ID to get the config for
|
||||
*/
|
||||
getOIDCConfig: async tenantId => {
|
||||
getOIDCConfigs: async tenantId => {
|
||||
return await API.get({
|
||||
url: `/api/global/configs/public/oidc?tenantId=${tenantId}`,
|
||||
})
|
||||
|
|
|
@ -68,13 +68,13 @@ export const createAPIClient = (config: APIClientConfig = {}): APIClient => {
|
|||
): Promise<APIError> => {
|
||||
// Try to read a message from the error
|
||||
let message = response.statusText
|
||||
let json: any = null
|
||||
let json = null
|
||||
try {
|
||||
json = await response.json()
|
||||
if (json?.message) {
|
||||
message = json.message
|
||||
} else if (json?.error) {
|
||||
message = json.error
|
||||
message = JSON.stringify(json.error)
|
||||
}
|
||||
} catch (error) {
|
||||
// Do nothing
|
||||
|
@ -93,7 +93,7 @@ export const createAPIClient = (config: APIClientConfig = {}): APIClient => {
|
|||
// Generates an error object from a string
|
||||
const makeError = (
|
||||
message: string,
|
||||
url?: string,
|
||||
url: string,
|
||||
method?: HTTPMethod
|
||||
): APIError => {
|
||||
return {
|
||||
|
@ -226,7 +226,7 @@ export const createAPIClient = (config: APIClientConfig = {}): APIClient => {
|
|||
return await handler(callConfig)
|
||||
} catch (error) {
|
||||
if (config?.onError) {
|
||||
config.onError(error)
|
||||
config.onError(error as APIError)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
@ -239,13 +239,9 @@ export const createAPIClient = (config: APIClientConfig = {}): APIClient => {
|
|||
patch: requestApiCall(HTTPMethod.PATCH),
|
||||
delete: requestApiCall(HTTPMethod.DELETE),
|
||||
put: requestApiCall(HTTPMethod.PUT),
|
||||
error: (message: string) => {
|
||||
throw makeError(message)
|
||||
},
|
||||
invalidateCache: () => {
|
||||
cache = {}
|
||||
},
|
||||
|
||||
// Generic utility to extract the current app ID. Assumes that any client
|
||||
// that exists in an app context will be attaching our app ID header.
|
||||
getAppID: (): string => {
|
||||
|
|
|
@ -46,7 +46,7 @@ export type Headers = Record<string, string>
|
|||
export type APIClientConfig = {
|
||||
enableCaching?: boolean
|
||||
attachHeaders?: (headers: Headers) => void
|
||||
onError?: (error: any) => void
|
||||
onError?: (error: APIError) => void
|
||||
onMigrationDetected?: (migration: string) => void
|
||||
}
|
||||
|
||||
|
@ -86,14 +86,13 @@ export type BaseAPIClient = {
|
|||
patch: <RequestT = null, ResponseT = void>(
|
||||
params: APICallParams<RequestT, ResponseT>
|
||||
) => Promise<ResponseT>
|
||||
error: (message: string) => void
|
||||
invalidateCache: () => void
|
||||
getAppID: () => string
|
||||
}
|
||||
|
||||
export type APIError = {
|
||||
message?: string
|
||||
url?: string
|
||||
url: string
|
||||
method?: HTTPMethod
|
||||
json: any
|
||||
status: number
|
||||
|
|
|
@ -3,7 +3,15 @@ import { BaseAPIClient } from "./types"
|
|||
|
||||
export interface ViewEndpoints {
|
||||
// Missing request or response types
|
||||
fetchViewData: (name: string, opts: any) => Promise<Row[]>
|
||||
fetchViewData: (
|
||||
name: string,
|
||||
opts: {
|
||||
calculation?: string
|
||||
field?: string
|
||||
groupBy?: string
|
||||
tableId: string
|
||||
}
|
||||
) => Promise<Row[]>
|
||||
exportView: (name: string, format: string) => Promise<any>
|
||||
saveView: (view: any) => Promise<any>
|
||||
deleteView: (name: string) => Promise<any>
|
||||
|
@ -20,7 +28,9 @@ export const buildViewEndpoints = (API: BaseAPIClient): ViewEndpoints => ({
|
|||
fetchViewData: async (name, { field, groupBy, calculation }) => {
|
||||
const params = new URLSearchParams()
|
||||
if (calculation) {
|
||||
if (field) {
|
||||
params.set("field", field)
|
||||
}
|
||||
params.set("calculation", calculation)
|
||||
}
|
||||
if (groupBy) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
CreateViewRequest,
|
||||
CreateViewResponse,
|
||||
PaginatedSearchRowResponse,
|
||||
SearchRowResponse,
|
||||
SearchViewRowRequest,
|
||||
UpdateViewRequest,
|
||||
|
@ -13,10 +14,14 @@ export interface ViewV2Endpoints {
|
|||
fetchDefinition: (viewId: string) => Promise<ViewResponseEnriched>
|
||||
create: (view: CreateViewRequest) => Promise<CreateViewResponse>
|
||||
update: (view: UpdateViewRequest) => Promise<UpdateViewResponse>
|
||||
fetch: (
|
||||
fetch: <T extends SearchViewRowRequest>(
|
||||
viewId: string,
|
||||
opts: SearchViewRowRequest
|
||||
) => Promise<SearchRowResponse>
|
||||
opts: T
|
||||
) => Promise<
|
||||
T extends { paginate: true }
|
||||
? PaginatedSearchRowResponse
|
||||
: SearchRowResponse
|
||||
>
|
||||
delete: (viewId: string) => Promise<void>
|
||||
}
|
||||
|
||||
|
@ -59,7 +64,7 @@ export const buildViewV2Endpoints = (API: BaseAPIClient): ViewV2Endpoints => ({
|
|||
* @param viewId the id of the view
|
||||
* @param opts the search options
|
||||
*/
|
||||
fetch: async (viewId, opts) => {
|
||||
fetch: async (viewId, opts: SearchViewRowRequest) => {
|
||||
return await API.post({
|
||||
url: `/api/v2/views/${encodeURIComponent(viewId)}/search`,
|
||||
body: opts,
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
: RelationshipType.MANY_TO_MANY,
|
||||
}
|
||||
|
||||
async function searchFunction(searchParams) {
|
||||
async function searchFunction(_tableId, searchParams) {
|
||||
if (
|
||||
subtype !== BBReferenceFieldSubType.USER &&
|
||||
subtype !== BBReferenceFieldSubType.USERS
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { FieldType } from "@budibase/types"
|
||||
import { FieldType, UIColumn } from "@budibase/types"
|
||||
|
||||
import OptionsCell from "../cells/OptionsCell.svelte"
|
||||
import DateCell from "../cells/DateCell.svelte"
|
||||
|
@ -40,13 +40,23 @@ const TypeComponentMap = {
|
|||
// Custom types for UI only
|
||||
role: RoleCell,
|
||||
}
|
||||
export const getCellRenderer = column => {
|
||||
|
||||
function getCellRendererByType(type: FieldType | "role" | undefined) {
|
||||
if (!type) {
|
||||
return
|
||||
}
|
||||
|
||||
return TypeComponentMap[type as keyof typeof TypeComponentMap]
|
||||
}
|
||||
|
||||
export const getCellRenderer = (column: UIColumn) => {
|
||||
if (column.calculationType) {
|
||||
return NumberCell
|
||||
}
|
||||
|
||||
return (
|
||||
TypeComponentMap[column?.schema?.cellRenderType] ||
|
||||
TypeComponentMap[column?.schema?.type] ||
|
||||
getCellRendererByType(column.schema?.cellRenderType) ||
|
||||
getCellRendererByType(column.schema?.type) ||
|
||||
TextCell
|
||||
)
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
// TODO: remove when all stores are typed
|
||||
|
||||
import { GeneratedIDPrefix, CellIDSeparator } from "./constants"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
|
||||
export const parseCellID = cellId => {
|
||||
if (!cellId) {
|
||||
return { rowId: undefined, field: undefined }
|
||||
}
|
||||
const parts = cellId.split(CellIDSeparator)
|
||||
const field = parts.pop()
|
||||
return { rowId: parts.join(CellIDSeparator), field }
|
||||
}
|
||||
|
||||
export const getCellID = (rowId, fieldName) => {
|
||||
return `${rowId}${CellIDSeparator}${fieldName}`
|
||||
}
|
||||
|
||||
export const parseEventLocation = e => {
|
||||
return {
|
||||
x: e.clientX ?? e.touches?.[0]?.clientX,
|
||||
y: e.clientY ?? e.touches?.[0]?.clientY,
|
||||
}
|
||||
}
|
||||
|
||||
export const generateRowID = () => {
|
||||
return `${GeneratedIDPrefix}${Helpers.uuid()}`
|
||||
}
|
||||
|
||||
export const isGeneratedRowID = id => {
|
||||
return id?.startsWith(GeneratedIDPrefix)
|
||||
}
|
|
@ -1,12 +1,14 @@
|
|||
import { get } from "svelte/store"
|
||||
import { createWebsocket } from "../../../utils"
|
||||
import { SocketEvent, GridSocketEvent } from "@budibase/shared-core"
|
||||
import { Store } from "../stores"
|
||||
import { UIDatasource, UIUser } from "@budibase/types"
|
||||
|
||||
export const createGridWebsocket = context => {
|
||||
export const createGridWebsocket = (context: Store) => {
|
||||
const { rows, datasource, users, focusedCellId, definition, API } = context
|
||||
const socket = createWebsocket("/socket/grid")
|
||||
|
||||
const connectToDatasource = datasource => {
|
||||
const connectToDatasource = (datasource: UIDatasource) => {
|
||||
if (!socket.connected) {
|
||||
return
|
||||
}
|
||||
|
@ -18,7 +20,7 @@ export const createGridWebsocket = context => {
|
|||
datasource,
|
||||
appId,
|
||||
},
|
||||
({ users: gridUsers }) => {
|
||||
({ users: gridUsers }: { users: UIUser[] }) => {
|
||||
users.set(gridUsers)
|
||||
}
|
||||
)
|
||||
|
@ -65,7 +67,7 @@ export const createGridWebsocket = context => {
|
|||
GridSocketEvent.DatasourceChange,
|
||||
({ datasource: newDatasource }) => {
|
||||
// Listen builder renames, as these aren't handled otherwise
|
||||
if (newDatasource?.name !== get(definition).name) {
|
||||
if (newDatasource?.name !== get(definition)?.name) {
|
||||
definition.set(newDatasource)
|
||||
}
|
||||
}
|
|
@ -69,7 +69,7 @@ export const deriveStores = (context: StoreContext): ConfigDerivedStore => {
|
|||
}
|
||||
|
||||
// Disable features for non DS+
|
||||
if (!["table", "viewV2"].includes(type)) {
|
||||
if (type && !["table", "viewV2"].includes(type)) {
|
||||
config.canAddRows = false
|
||||
config.canEditRows = false
|
||||
config.canDeleteRows = false
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// TODO: datasource and defitions are unions of the different implementations. At this point, the datasource does not know what type is being used, and the assignations will cause TS exceptions. Casting it "as any" for now. This should be fixed improving the type usages.
|
||||
|
||||
import { derived, get, Readable, Writable } from "svelte/store"
|
||||
import { getDatasourceDefinition, getDatasourceSchema } from "../../../fetch"
|
||||
import { enrichSchemaWithRelColumns, memo } from "../../../utils"
|
||||
|
@ -71,10 +73,10 @@ export const deriveStores = (context: StoreContext): DerivedDatasourceStore => {
|
|||
} = context
|
||||
|
||||
const schema = derived(definition, $definition => {
|
||||
let schema: Record<string, UIFieldSchema> = getDatasourceSchema({
|
||||
const schema: Record<string, any> | undefined = getDatasourceSchema({
|
||||
API,
|
||||
datasource: get(datasource),
|
||||
definition: $definition,
|
||||
datasource: get(datasource) as any, // TODO: see line 1
|
||||
definition: $definition ?? undefined,
|
||||
})
|
||||
if (!schema) {
|
||||
return null
|
||||
|
@ -82,7 +84,7 @@ export const deriveStores = (context: StoreContext): DerivedDatasourceStore => {
|
|||
|
||||
// Ensure schema is configured as objects.
|
||||
// Certain datasources like queries use primitives.
|
||||
Object.keys(schema || {}).forEach(key => {
|
||||
Object.keys(schema).forEach(key => {
|
||||
if (typeof schema[key] !== "object") {
|
||||
schema[key] = { name: key, type: schema[key] }
|
||||
}
|
||||
|
@ -130,13 +132,13 @@ export const deriveStores = (context: StoreContext): DerivedDatasourceStore => {
|
|||
([$datasource, $definition]) => {
|
||||
let type = $datasource?.type
|
||||
if (type === "provider") {
|
||||
type = ($datasource as any).value?.datasource?.type
|
||||
type = ($datasource as any).value?.datasource?.type // TODO: see line 1
|
||||
}
|
||||
// Handle calculation views
|
||||
if (type === "viewV2" && $definition?.type === ViewV2Type.CALCULATION) {
|
||||
return false
|
||||
}
|
||||
return ["table", "viewV2", "link"].includes(type)
|
||||
return !!type && ["table", "viewV2", "link"].includes(type)
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -184,9 +186,9 @@ export const createActions = (context: StoreContext): ActionDatasourceStore => {
|
|||
const refreshDefinition = async () => {
|
||||
const def = await getDatasourceDefinition({
|
||||
API,
|
||||
datasource: get(datasource),
|
||||
datasource: get(datasource) as any, // TODO: see line 1
|
||||
})
|
||||
definition.set(def)
|
||||
definition.set(def as any) // TODO: see line 1
|
||||
}
|
||||
|
||||
// Saves the datasource definition
|
||||
|
@ -231,7 +233,7 @@ export const createActions = (context: StoreContext): ActionDatasourceStore => {
|
|||
if ("default" in newDefinition.schema[column]) {
|
||||
delete newDefinition.schema[column].default
|
||||
}
|
||||
return await saveDefinition(newDefinition as any)
|
||||
return await saveDefinition(newDefinition as any) // TODO: see line 1
|
||||
}
|
||||
|
||||
// Adds a schema mutation for a single field
|
||||
|
@ -307,7 +309,7 @@ export const createActions = (context: StoreContext): ActionDatasourceStore => {
|
|||
await saveDefinition({
|
||||
...$definition,
|
||||
schema: newSchema,
|
||||
} as any)
|
||||
} as any) // TODO: see line 1
|
||||
resetSchemaMutations()
|
||||
}
|
||||
|
||||
|
|
|
@ -10,9 +10,10 @@ import {
|
|||
import { tick } from "svelte"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
import { sleep } from "../../../utils/utils"
|
||||
import { FieldType, Row, UIFetchAPI, UIRow } from "@budibase/types"
|
||||
import { FieldType, Row, UIRow } from "@budibase/types"
|
||||
import { getRelatedTableValues } from "../../../utils"
|
||||
import { Store as StoreContext } from "."
|
||||
import DataFetch from "../../../fetch/DataFetch"
|
||||
|
||||
interface IndexedUIRow extends UIRow {
|
||||
__idx: number
|
||||
|
@ -20,7 +21,7 @@ interface IndexedUIRow extends UIRow {
|
|||
|
||||
interface RowStore {
|
||||
rows: Writable<UIRow[]>
|
||||
fetch: Writable<UIFetchAPI | null>
|
||||
fetch: Writable<DataFetch<any, any, any> | null> // TODO: type this properly, having a union of all the possible options
|
||||
loaded: Writable<boolean>
|
||||
refreshing: Writable<boolean>
|
||||
loading: Writable<boolean>
|
||||
|
@ -225,7 +226,7 @@ export const createActions = (context: StoreContext): RowActionStore => {
|
|||
})
|
||||
|
||||
// Subscribe to changes of this fetch model
|
||||
unsubscribe = newFetch.subscribe(async ($fetch: UIFetchAPI) => {
|
||||
unsubscribe = newFetch.subscribe(async $fetch => {
|
||||
if ($fetch.error) {
|
||||
// Present a helpful error to the user
|
||||
let message = "An unknown error occurred"
|
||||
|
@ -253,7 +254,7 @@ export const createActions = (context: StoreContext): RowActionStore => {
|
|||
|
||||
// Reset state properties when dataset changes
|
||||
if (!$instanceLoaded || resetRows) {
|
||||
definition.set($fetch.definition)
|
||||
definition.set($fetch.definition as any) // TODO: datasource and defitions are unions of the different implementations. At this point, the datasource does not know what type is being used, and the assignations will cause TS exceptions. Casting it "as any" for now. This should be fixed improving the type usages.
|
||||
}
|
||||
|
||||
// Reset scroll state when data changes
|
||||
|
|
|
@ -32,8 +32,8 @@ export const Cookies = {
|
|||
}
|
||||
|
||||
// Table names
|
||||
export const TableNames = {
|
||||
USERS: "ta_users",
|
||||
export const enum TableNames {
|
||||
USERS = "ta_users",
|
||||
}
|
||||
|
||||
export const BudibaseRoles = {
|
||||
|
|
|
@ -1,8 +1,18 @@
|
|||
import DataFetch from "./DataFetch.js"
|
||||
import DataFetch from "./DataFetch"
|
||||
|
||||
export default class CustomFetch extends DataFetch {
|
||||
interface CustomDatasource {
|
||||
type: "custom"
|
||||
data: any
|
||||
}
|
||||
|
||||
type CustomDefinition = Record<string, any>
|
||||
|
||||
export default class CustomFetch extends DataFetch<
|
||||
CustomDatasource,
|
||||
CustomDefinition
|
||||
> {
|
||||
// Gets the correct Budibase type for a JS value
|
||||
getType(value) {
|
||||
getType(value: any) {
|
||||
if (value == null) {
|
||||
return "string"
|
||||
}
|
||||
|
@ -22,7 +32,7 @@ export default class CustomFetch extends DataFetch {
|
|||
}
|
||||
|
||||
// Parses the custom data into an array format
|
||||
parseCustomData(data) {
|
||||
parseCustomData(data: any) {
|
||||
if (!data) {
|
||||
return []
|
||||
}
|
||||
|
@ -55,7 +65,7 @@ export default class CustomFetch extends DataFetch {
|
|||
}
|
||||
|
||||
// Enriches the custom data to ensure the structure and format is usable
|
||||
enrichCustomData(data) {
|
||||
enrichCustomData(data: (string | any)[]) {
|
||||
if (!data?.length) {
|
||||
return []
|
||||
}
|
||||
|
@ -72,7 +82,7 @@ export default class CustomFetch extends DataFetch {
|
|||
// Try parsing strings
|
||||
if (typeof value === "string") {
|
||||
const split = value.split(",").map(x => x.trim())
|
||||
let obj = {}
|
||||
const obj: Record<string, string> = {}
|
||||
for (let i = 0; i < split.length; i++) {
|
||||
const suffix = i === 0 ? "" : ` ${i + 1}`
|
||||
const key = `Value${suffix}`
|
||||
|
@ -87,27 +97,29 @@ export default class CustomFetch extends DataFetch {
|
|||
}
|
||||
|
||||
// Extracts and parses the custom data from the datasource definition
|
||||
getCustomData(datasource) {
|
||||
getCustomData(datasource: CustomDatasource) {
|
||||
return this.enrichCustomData(this.parseCustomData(datasource?.data))
|
||||
}
|
||||
|
||||
async getDefinition(datasource) {
|
||||
async getDefinition() {
|
||||
const { datasource } = this.options
|
||||
|
||||
// Try and work out the schema from the array provided
|
||||
let schema = {}
|
||||
const schema: CustomDefinition = {}
|
||||
const data = this.getCustomData(datasource)
|
||||
if (!data?.length) {
|
||||
return { schema }
|
||||
}
|
||||
|
||||
// Go through every object and extract all valid keys
|
||||
for (let datum of data) {
|
||||
for (let key of Object.keys(datum)) {
|
||||
for (const datum of data) {
|
||||
for (const key of Object.keys(datum)) {
|
||||
if (key === "_id") {
|
||||
continue
|
||||
}
|
||||
if (!schema[key]) {
|
||||
let type = this.getType(datum[key])
|
||||
let constraints = {}
|
||||
const constraints: any = {}
|
||||
|
||||
// Determine whether we should render text columns as options instead
|
||||
if (type === "string") {
|
|
@ -1,25 +1,103 @@
|
|||
import { writable, derived, get } from "svelte/store"
|
||||
import { writable, derived, get, Writable, Readable } from "svelte/store"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { QueryUtils } from "../utils"
|
||||
import { convertJSONSchemaToTableSchema } from "../utils/json"
|
||||
import { FieldType, SortOrder, SortType } from "@budibase/types"
|
||||
import {
|
||||
FieldType,
|
||||
LegacyFilter,
|
||||
Row,
|
||||
SearchFilters,
|
||||
SortOrder,
|
||||
SortType,
|
||||
TableSchema,
|
||||
UISearchFilter,
|
||||
} from "@budibase/types"
|
||||
import { APIClient } from "../api/types"
|
||||
import { DataFetchType } from "."
|
||||
|
||||
const { buildQuery, limit: queryLimit, runQuery, sort } = QueryUtils
|
||||
|
||||
interface DataFetchStore<TDefinition, TQuery> {
|
||||
rows: Row[]
|
||||
info: any
|
||||
schema: TableSchema | null
|
||||
loading: boolean
|
||||
loaded: boolean
|
||||
query: TQuery
|
||||
pageNumber: number
|
||||
cursor: string | null
|
||||
cursors: string[]
|
||||
resetKey: string
|
||||
error: {
|
||||
message: string
|
||||
status: number
|
||||
} | null
|
||||
definition?: TDefinition | null
|
||||
}
|
||||
|
||||
interface DataFetchDerivedStore<TDefinition, TQuery>
|
||||
extends DataFetchStore<TDefinition, TQuery> {
|
||||
hasNextPage: boolean
|
||||
hasPrevPage: boolean
|
||||
supportsSearch: boolean
|
||||
supportsSort: boolean
|
||||
supportsPagination: boolean
|
||||
}
|
||||
|
||||
export interface DataFetchParams<
|
||||
TDatasource,
|
||||
TQuery = SearchFilters | undefined
|
||||
> {
|
||||
API: APIClient
|
||||
datasource: TDatasource
|
||||
query: TQuery
|
||||
options?: {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parent class which handles the implementation of fetching data from an
|
||||
* internal table or datasource plus.
|
||||
* For other types of datasource, this class is overridden and extended.
|
||||
*/
|
||||
export default class DataFetch {
|
||||
export default abstract class DataFetch<
|
||||
TDatasource extends { type: DataFetchType },
|
||||
TDefinition extends {
|
||||
schema?: Record<string, any> | null
|
||||
primaryDisplay?: string
|
||||
},
|
||||
TQuery extends {} = SearchFilters
|
||||
> {
|
||||
API: APIClient
|
||||
features: {
|
||||
supportsSearch: boolean
|
||||
supportsSort: boolean
|
||||
supportsPagination: boolean
|
||||
}
|
||||
options: {
|
||||
datasource: TDatasource
|
||||
limit: number
|
||||
// Search config
|
||||
filter: UISearchFilter | LegacyFilter[] | null
|
||||
query: TQuery
|
||||
// Sorting config
|
||||
sortColumn: string | null
|
||||
sortOrder: SortOrder
|
||||
sortType: SortType | null
|
||||
// Pagination config
|
||||
paginate: boolean
|
||||
// Client side feature customisation
|
||||
clientSideSearching: boolean
|
||||
clientSideSorting: boolean
|
||||
clientSideLimiting: boolean
|
||||
}
|
||||
store: Writable<DataFetchStore<TDefinition, TQuery>>
|
||||
derivedStore: Readable<DataFetchDerivedStore<TDefinition, TQuery>>
|
||||
|
||||
/**
|
||||
* Constructs a new DataFetch instance.
|
||||
* @param opts the fetch options
|
||||
*/
|
||||
constructor(opts) {
|
||||
// API client
|
||||
this.API = null
|
||||
|
||||
constructor(opts: DataFetchParams<TDatasource, TQuery>) {
|
||||
// Feature flags
|
||||
this.features = {
|
||||
supportsSearch: false,
|
||||
|
@ -29,12 +107,12 @@ export default class DataFetch {
|
|||
|
||||
// Config
|
||||
this.options = {
|
||||
datasource: null,
|
||||
datasource: opts.datasource,
|
||||
limit: 10,
|
||||
|
||||
// Search config
|
||||
filter: null,
|
||||
query: null,
|
||||
query: opts.query,
|
||||
|
||||
// Sorting config
|
||||
sortColumn: null,
|
||||
|
@ -57,11 +135,11 @@ export default class DataFetch {
|
|||
schema: null,
|
||||
loading: false,
|
||||
loaded: false,
|
||||
query: null,
|
||||
query: opts.query,
|
||||
pageNumber: 0,
|
||||
cursor: null,
|
||||
cursors: [],
|
||||
resetKey: Math.random(),
|
||||
resetKey: Math.random().toString(),
|
||||
error: null,
|
||||
})
|
||||
|
||||
|
@ -102,9 +180,6 @@ export default class DataFetch {
|
|||
this.store.update($store => ({ ...$store, loaded: true }))
|
||||
return
|
||||
}
|
||||
|
||||
// Initially fetch data but don't bother waiting for the result
|
||||
this.getInitialData()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -118,7 +193,10 @@ export default class DataFetch {
|
|||
/**
|
||||
* Gets the default sort column for this datasource
|
||||
*/
|
||||
getDefaultSortColumn(definition, schema) {
|
||||
getDefaultSortColumn(
|
||||
definition: { primaryDisplay?: string } | null,
|
||||
schema: Record<string, any>
|
||||
): string | null {
|
||||
if (definition?.primaryDisplay && schema[definition.primaryDisplay]) {
|
||||
return definition.primaryDisplay
|
||||
} else {
|
||||
|
@ -130,13 +208,13 @@ export default class DataFetch {
|
|||
* Fetches a fresh set of data from the server, resetting pagination
|
||||
*/
|
||||
async getInitialData() {
|
||||
const { datasource, filter, paginate } = this.options
|
||||
const { filter, paginate } = this.options
|
||||
|
||||
// Fetch datasource definition and extract sort properties if configured
|
||||
const definition = await this.getDefinition(datasource)
|
||||
const definition = await this.getDefinition()
|
||||
|
||||
// Determine feature flags
|
||||
const features = this.determineFeatureFlags(definition)
|
||||
const features = await this.determineFeatureFlags()
|
||||
this.features = {
|
||||
supportsSearch: !!features?.supportsSearch,
|
||||
supportsSort: !!features?.supportsSort,
|
||||
|
@ -144,11 +222,11 @@ export default class DataFetch {
|
|||
}
|
||||
|
||||
// Fetch and enrich schema
|
||||
let schema = this.getSchema(datasource, definition)
|
||||
schema = this.enrichSchema(schema)
|
||||
let schema = this.getSchema(definition)
|
||||
if (!schema) {
|
||||
return
|
||||
}
|
||||
schema = this.enrichSchema(schema)
|
||||
|
||||
// If an invalid sort column is specified, delete it
|
||||
if (this.options.sortColumn && !schema[this.options.sortColumn]) {
|
||||
|
@ -172,20 +250,25 @@ export default class DataFetch {
|
|||
if (
|
||||
fieldSchema?.type === FieldType.NUMBER ||
|
||||
fieldSchema?.type === FieldType.BIGINT ||
|
||||
fieldSchema?.calculationType
|
||||
("calculationType" in fieldSchema && fieldSchema?.calculationType)
|
||||
) {
|
||||
this.options.sortType = SortType.NUMBER
|
||||
}
|
||||
|
||||
// If no sort order, default to ascending
|
||||
if (!this.options.sortOrder) {
|
||||
this.options.sortOrder = SortOrder.ASCENDING
|
||||
} else {
|
||||
// Ensure sortOrder matches the enum
|
||||
this.options.sortOrder =
|
||||
this.options.sortOrder.toLowerCase() as SortOrder
|
||||
}
|
||||
}
|
||||
|
||||
// Build the query
|
||||
let query = this.options.query
|
||||
if (!query) {
|
||||
query = buildQuery(filter)
|
||||
query = buildQuery(filter ?? undefined) as TQuery
|
||||
}
|
||||
|
||||
// Update store
|
||||
|
@ -210,7 +293,7 @@ export default class DataFetch {
|
|||
info: page.info,
|
||||
cursors: paginate && page.hasNextPage ? [null, page.cursor] : [null],
|
||||
error: page.error,
|
||||
resetKey: Math.random(),
|
||||
resetKey: Math.random().toString(),
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -238,8 +321,8 @@ export default class DataFetch {
|
|||
}
|
||||
|
||||
// If we don't support sorting, do a client-side sort
|
||||
if (!this.features.supportsSort && clientSideSorting) {
|
||||
rows = sort(rows, sortColumn, sortOrder, sortType)
|
||||
if (!this.features.supportsSort && clientSideSorting && sortType) {
|
||||
rows = sort(rows, sortColumn as any, sortOrder, sortType)
|
||||
}
|
||||
|
||||
// If we don't support pagination, do a client-side limit
|
||||
|
@ -256,49 +339,28 @@ export default class DataFetch {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a single page of data from the remote resource.
|
||||
* Must be overridden by a datasource specific child class.
|
||||
*/
|
||||
async getData() {
|
||||
return {
|
||||
rows: [],
|
||||
info: null,
|
||||
hasNextPage: false,
|
||||
cursor: null,
|
||||
}
|
||||
}
|
||||
abstract getData(): Promise<{
|
||||
rows: Row[]
|
||||
info?: any
|
||||
hasNextPage?: boolean
|
||||
cursor?: any
|
||||
error?: any
|
||||
}>
|
||||
|
||||
/**
|
||||
* Gets the definition for this datasource.
|
||||
* Defaults to fetching a table definition.
|
||||
* @param datasource
|
||||
|
||||
* @return {object} the definition
|
||||
*/
|
||||
async getDefinition(datasource) {
|
||||
if (!datasource?.tableId) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
return await this.API.fetchTableDefinition(datasource.tableId)
|
||||
} catch (error) {
|
||||
this.store.update(state => ({
|
||||
...state,
|
||||
error,
|
||||
}))
|
||||
return null
|
||||
}
|
||||
}
|
||||
abstract getDefinition(): Promise<TDefinition | null>
|
||||
|
||||
/**
|
||||
* Gets the schema definition for a datasource.
|
||||
* Defaults to getting the "schema" property of the definition.
|
||||
* @param datasource the datasource
|
||||
* @param definition the datasource definition
|
||||
* @return {object} the schema
|
||||
*/
|
||||
getSchema(datasource, definition) {
|
||||
return definition?.schema
|
||||
getSchema(definition: TDefinition | null): Record<string, any> | undefined {
|
||||
return definition?.schema ?? undefined
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -307,32 +369,30 @@ export default class DataFetch {
|
|||
* @param schema the datasource schema
|
||||
* @return {object} the enriched datasource schema
|
||||
*/
|
||||
enrichSchema(schema) {
|
||||
if (schema == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
enrichSchema(schema: TableSchema): TableSchema {
|
||||
// Check for any JSON fields so we can add any top level properties
|
||||
let jsonAdditions = {}
|
||||
Object.keys(schema).forEach(fieldKey => {
|
||||
let jsonAdditions: Record<string, { type: string; nestedJSON: true }> = {}
|
||||
for (const fieldKey of Object.keys(schema)) {
|
||||
const fieldSchema = schema[fieldKey]
|
||||
if (fieldSchema?.type === FieldType.JSON) {
|
||||
if (fieldSchema.type === FieldType.JSON) {
|
||||
const jsonSchema = convertJSONSchemaToTableSchema(fieldSchema, {
|
||||
squashObjects: true,
|
||||
})
|
||||
Object.keys(jsonSchema).forEach(jsonKey => {
|
||||
}) as Record<string, { type: string }> | null // TODO: remove when convertJSONSchemaToTableSchema is typed
|
||||
if (jsonSchema) {
|
||||
for (const jsonKey of Object.keys(jsonSchema)) {
|
||||
jsonAdditions[`${fieldKey}.${jsonKey}`] = {
|
||||
type: jsonSchema[jsonKey].type,
|
||||
nestedJSON: true,
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
schema = { ...schema, ...jsonAdditions }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure schema is in the correct structure
|
||||
let enrichedSchema = {}
|
||||
Object.entries(schema).forEach(([fieldName, fieldSchema]) => {
|
||||
let enrichedSchema: TableSchema = {}
|
||||
Object.entries({ ...schema, ...jsonAdditions }).forEach(
|
||||
([fieldName, fieldSchema]) => {
|
||||
if (typeof fieldSchema === "string") {
|
||||
enrichedSchema[fieldName] = {
|
||||
type: fieldSchema,
|
||||
|
@ -341,19 +401,24 @@ export default class DataFetch {
|
|||
} else {
|
||||
enrichedSchema[fieldName] = {
|
||||
...fieldSchema,
|
||||
type: fieldSchema.type as any, // TODO: check type union definition conflicts
|
||||
name: fieldName,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
return enrichedSchema
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the feature flag for this datasource definition
|
||||
* @param definition
|
||||
* Determine the feature flag for this datasource
|
||||
*/
|
||||
determineFeatureFlags(_definition) {
|
||||
async determineFeatureFlags(): Promise<{
|
||||
supportsPagination: boolean
|
||||
supportsSearch?: boolean
|
||||
supportsSort?: boolean
|
||||
}> {
|
||||
return {
|
||||
supportsSearch: false,
|
||||
supportsSort: false,
|
||||
|
@ -365,12 +430,11 @@ export default class DataFetch {
|
|||
* Resets the data set and updates options
|
||||
* @param newOptions any new options
|
||||
*/
|
||||
async update(newOptions) {
|
||||
async update(newOptions: any) {
|
||||
// Check if any settings have actually changed
|
||||
let refresh = false
|
||||
const entries = Object.entries(newOptions || {})
|
||||
for (let [key, value] of entries) {
|
||||
const oldVal = this.options[key] == null ? null : this.options[key]
|
||||
for (const [key, value] of Object.entries(newOptions || {})) {
|
||||
const oldVal = this.options[key as keyof typeof this.options] ?? null
|
||||
const newVal = value == null ? null : value
|
||||
if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
|
||||
refresh = true
|
||||
|
@ -437,7 +501,7 @@ export default class DataFetch {
|
|||
* @param state the current store state
|
||||
* @return {boolean} whether there is a next page of data or not
|
||||
*/
|
||||
hasNextPage(state) {
|
||||
private hasNextPage(state: DataFetchStore<TDefinition, TQuery>): boolean {
|
||||
return state.cursors[state.pageNumber + 1] != null
|
||||
}
|
||||
|
||||
|
@ -447,7 +511,7 @@ export default class DataFetch {
|
|||
* @param state the current store state
|
||||
* @return {boolean} whether there is a previous page of data or not
|
||||
*/
|
||||
hasPrevPage(state) {
|
||||
private hasPrevPage(state: { pageNumber: number }): boolean {
|
||||
return state.pageNumber > 0
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
import DataFetch from "./DataFetch.js"
|
||||
|
||||
export default class FieldFetch extends DataFetch {
|
||||
async getDefinition(datasource) {
|
||||
// Field sources have their schema statically defined
|
||||
let schema
|
||||
if (datasource.fieldType === "attachment") {
|
||||
schema = {
|
||||
url: {
|
||||
type: "string",
|
||||
},
|
||||
name: {
|
||||
type: "string",
|
||||
},
|
||||
}
|
||||
} else if (datasource.fieldType === "array") {
|
||||
schema = {
|
||||
value: {
|
||||
type: "string",
|
||||
},
|
||||
}
|
||||
}
|
||||
return { schema }
|
||||
}
|
||||
|
||||
async getData() {
|
||||
const { datasource } = this.options
|
||||
|
||||
// These sources will be available directly from context
|
||||
const data = datasource?.value || []
|
||||
let rows
|
||||
if (Array.isArray(data) && data[0] && typeof data[0] !== "object") {
|
||||
rows = data.map(value => ({ value }))
|
||||
} else {
|
||||
rows = data
|
||||
}
|
||||
|
||||
return {
|
||||
rows: rows || [],
|
||||
hasNextPage: false,
|
||||
cursor: null,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
import { Row } from "@budibase/types"
|
||||
import DataFetch from "./DataFetch"
|
||||
|
||||
type Types = "field" | "queryarray" | "jsonarray"
|
||||
|
||||
export interface FieldDatasource<TType extends Types> {
|
||||
type: TType
|
||||
tableId: string
|
||||
fieldType: "attachment" | "array"
|
||||
value: string[] | Row[]
|
||||
}
|
||||
|
||||
export interface FieldDefinition {
|
||||
schema?: Record<string, { type: string }> | null
|
||||
}
|
||||
|
||||
function isArrayOfStrings(value: string[] | Row[]): value is string[] {
|
||||
return Array.isArray(value) && !!value[0] && typeof value[0] !== "object"
|
||||
}
|
||||
|
||||
export default class FieldFetch<TType extends Types> extends DataFetch<
|
||||
FieldDatasource<TType>,
|
||||
FieldDefinition
|
||||
> {
|
||||
async getDefinition(): Promise<FieldDefinition | null> {
|
||||
const { datasource } = this.options
|
||||
|
||||
// Field sources have their schema statically defined
|
||||
let schema
|
||||
if (datasource.fieldType === "attachment") {
|
||||
schema = {
|
||||
url: {
|
||||
type: "string",
|
||||
},
|
||||
name: {
|
||||
type: "string",
|
||||
},
|
||||
}
|
||||
} else if (datasource.fieldType === "array") {
|
||||
schema = {
|
||||
value: {
|
||||
type: "string",
|
||||
},
|
||||
}
|
||||
}
|
||||
return { schema }
|
||||
}
|
||||
|
||||
async getData() {
|
||||
const { datasource } = this.options
|
||||
|
||||
// These sources will be available directly from context
|
||||
const data = datasource?.value || []
|
||||
let rows: Row[]
|
||||
if (isArrayOfStrings(data)) {
|
||||
rows = data.map(value => ({ value }))
|
||||
} else {
|
||||
rows = data
|
||||
}
|
||||
|
||||
return {
|
||||
rows: rows || [],
|
||||
hasNextPage: false,
|
||||
cursor: null,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +1,33 @@
|
|||
import { get } from "svelte/store"
|
||||
import DataFetch from "./DataFetch.js"
|
||||
import DataFetch, { DataFetchParams } from "./DataFetch"
|
||||
import { TableNames } from "../constants"
|
||||
|
||||
export default class GroupUserFetch extends DataFetch {
|
||||
constructor(opts) {
|
||||
interface GroupUserQuery {
|
||||
groupId: string
|
||||
emailSearch: string
|
||||
}
|
||||
|
||||
interface GroupUserDatasource {
|
||||
type: "groupUser"
|
||||
tableId: TableNames.USERS
|
||||
}
|
||||
|
||||
export default class GroupUserFetch extends DataFetch<
|
||||
GroupUserDatasource,
|
||||
{},
|
||||
GroupUserQuery
|
||||
> {
|
||||
constructor(opts: DataFetchParams<GroupUserDatasource, GroupUserQuery>) {
|
||||
super({
|
||||
...opts,
|
||||
datasource: {
|
||||
type: "groupUser",
|
||||
tableId: TableNames.USERS,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
determineFeatureFlags() {
|
||||
async determineFeatureFlags() {
|
||||
return {
|
||||
supportsSearch: true,
|
||||
supportsSort: false,
|
||||
|
@ -28,11 +43,12 @@ export default class GroupUserFetch extends DataFetch {
|
|||
|
||||
async getData() {
|
||||
const { query, cursor } = get(this.store)
|
||||
|
||||
try {
|
||||
const res = await this.API.getGroupUsers({
|
||||
id: query.groupId,
|
||||
emailSearch: query.emailSearch,
|
||||
bookmark: cursor,
|
||||
bookmark: cursor ?? undefined,
|
||||
})
|
||||
|
||||
return {
|
|
@ -1,8 +1,10 @@
|
|||
import FieldFetch from "./FieldFetch.js"
|
||||
import FieldFetch from "./FieldFetch"
|
||||
import { getJSONArrayDatasourceSchema } from "../utils/json"
|
||||
|
||||
export default class JSONArrayFetch extends FieldFetch {
|
||||
async getDefinition(datasource) {
|
||||
export default class JSONArrayFetch extends FieldFetch<"jsonarray"> {
|
||||
async getDefinition() {
|
||||
const { datasource } = this.options
|
||||
|
||||
// JSON arrays need their table definitions fetched.
|
||||
// We can then extract their schema as a subset of the table schema.
|
||||
try {
|
|
@ -1,21 +0,0 @@
|
|||
import DataFetch from "./DataFetch.js"
|
||||
|
||||
export default class NestedProviderFetch extends DataFetch {
|
||||
async getDefinition(datasource) {
|
||||
// Nested providers should already have exposed their own schema
|
||||
return {
|
||||
schema: datasource?.value?.schema,
|
||||
primaryDisplay: datasource?.value?.primaryDisplay,
|
||||
}
|
||||
}
|
||||
|
||||
async getData() {
|
||||
const { datasource } = this.options
|
||||
// Pull the rows from the existing data provider
|
||||
return {
|
||||
rows: datasource?.value?.rows || [],
|
||||
hasNextPage: false,
|
||||
cursor: null,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import { Row, TableSchema } from "@budibase/types"
|
||||
import DataFetch from "./DataFetch"
|
||||
|
||||
interface NestedProviderDatasource {
|
||||
type: "provider"
|
||||
value?: {
|
||||
schema: TableSchema
|
||||
primaryDisplay: string
|
||||
rows: Row[]
|
||||
}
|
||||
}
|
||||
|
||||
interface NestedProviderDefinition {
|
||||
schema?: TableSchema
|
||||
primaryDisplay?: string
|
||||
}
|
||||
export default class NestedProviderFetch extends DataFetch<
|
||||
NestedProviderDatasource,
|
||||
NestedProviderDefinition
|
||||
> {
|
||||
async getDefinition() {
|
||||
const { datasource } = this.options
|
||||
|
||||
// Nested providers should already have exposed their own schema
|
||||
return {
|
||||
schema: datasource?.value?.schema,
|
||||
primaryDisplay: datasource?.value?.primaryDisplay,
|
||||
}
|
||||
}
|
||||
|
||||
async getData() {
|
||||
const { datasource } = this.options
|
||||
// Pull the rows from the existing data provider
|
||||
return {
|
||||
rows: datasource?.value?.rows || [],
|
||||
hasNextPage: false,
|
||||
cursor: null,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,13 @@
|
|||
import FieldFetch from "./FieldFetch.js"
|
||||
import FieldFetch from "./FieldFetch"
|
||||
import {
|
||||
getJSONArrayDatasourceSchema,
|
||||
generateQueryArraySchemas,
|
||||
} from "../utils/json"
|
||||
|
||||
export default class QueryArrayFetch extends FieldFetch {
|
||||
async getDefinition(datasource) {
|
||||
export default class QueryArrayFetch extends FieldFetch<"queryarray"> {
|
||||
async getDefinition() {
|
||||
const { datasource } = this.options
|
||||
|
||||
if (!datasource?.tableId) {
|
||||
return null
|
||||
}
|
||||
|
@ -14,10 +16,14 @@ export default class QueryArrayFetch extends FieldFetch {
|
|||
try {
|
||||
const table = await this.API.fetchQueryDefinition(datasource.tableId)
|
||||
const schema = generateQueryArraySchemas(
|
||||
table?.schema,
|
||||
table?.nestedSchemaFields
|
||||
table.schema,
|
||||
table.nestedSchemaFields
|
||||
)
|
||||
return { schema: getJSONArrayDatasourceSchema(schema, datasource) }
|
||||
const result = {
|
||||
schema: getJSONArrayDatasourceSchema(schema, datasource),
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
|
@ -1,9 +1,25 @@
|
|||
import DataFetch from "./DataFetch.js"
|
||||
import DataFetch from "./DataFetch"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
import { ExecuteQueryRequest, Query } from "@budibase/types"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
export default class QueryFetch extends DataFetch {
|
||||
determineFeatureFlags(definition) {
|
||||
interface QueryDatasource {
|
||||
type: "query"
|
||||
_id: string
|
||||
fields: Record<string, any> & {
|
||||
pagination?: {
|
||||
type: string
|
||||
location: string
|
||||
pageParam: string
|
||||
}
|
||||
}
|
||||
queryParams?: Record<string, string>
|
||||
parameters: { name: string; default: string }[]
|
||||
}
|
||||
|
||||
export default class QueryFetch extends DataFetch<QueryDatasource, Query> {
|
||||
async determineFeatureFlags() {
|
||||
const definition = await this.getDefinition()
|
||||
const supportsPagination =
|
||||
!!definition?.fields?.pagination?.type &&
|
||||
!!definition?.fields?.pagination?.location &&
|
||||
|
@ -11,7 +27,9 @@ export default class QueryFetch extends DataFetch {
|
|||
return { supportsPagination }
|
||||
}
|
||||
|
||||
async getDefinition(datasource) {
|
||||
async getDefinition() {
|
||||
const { datasource } = this.options
|
||||
|
||||
if (!datasource?._id) {
|
||||
return null
|
||||
}
|
||||
|
@ -40,17 +58,17 @@ export default class QueryFetch extends DataFetch {
|
|||
const type = definition?.fields?.pagination?.type
|
||||
|
||||
// Set the default query params
|
||||
let parameters = Helpers.cloneDeep(datasource?.queryParams || {})
|
||||
for (let param of datasource?.parameters || {}) {
|
||||
const parameters = Helpers.cloneDeep(datasource.queryParams || {})
|
||||
for (const param of datasource?.parameters || []) {
|
||||
if (!parameters[param.name]) {
|
||||
parameters[param.name] = param.default
|
||||
}
|
||||
}
|
||||
|
||||
// Add pagination to query if supported
|
||||
let queryPayload = { parameters }
|
||||
const queryPayload: ExecuteQueryRequest = { parameters }
|
||||
if (paginate && supportsPagination) {
|
||||
const requestCursor = type === "page" ? parseInt(cursor || 1) : cursor
|
||||
const requestCursor = type === "page" ? parseInt(cursor || "1") : cursor
|
||||
queryPayload.pagination = { page: requestCursor, limit }
|
||||
}
|
||||
|
||||
|
@ -65,7 +83,7 @@ export default class QueryFetch extends DataFetch {
|
|||
if (paginate && supportsPagination) {
|
||||
if (type === "page") {
|
||||
// For "page number" pagination, increment the existing page number
|
||||
nextCursor = queryPayload.pagination.page + 1
|
||||
nextCursor = queryPayload.pagination!.page! + 1
|
||||
hasNextPage = data?.length === limit && limit > 0
|
||||
} else {
|
||||
// For "cursor" pagination, the cursor should be in the response
|
|
@ -1,20 +0,0 @@
|
|||
import DataFetch from "./DataFetch.js"
|
||||
|
||||
export default class RelationshipFetch extends DataFetch {
|
||||
async getData() {
|
||||
const { datasource } = this.options
|
||||
if (!datasource?.rowId || !datasource?.rowTableId) {
|
||||
return { rows: [] }
|
||||
}
|
||||
try {
|
||||
const res = await this.API.fetchRelationshipData(
|
||||
datasource.rowTableId,
|
||||
datasource.rowId,
|
||||
datasource.fieldName
|
||||
)
|
||||
return { rows: res }
|
||||
} catch (error) {
|
||||
return { rows: [] }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import { Table } from "@budibase/types"
|
||||
import DataFetch from "./DataFetch"
|
||||
|
||||
interface RelationshipDatasource {
|
||||
type: "link"
|
||||
tableId: string
|
||||
rowId: string
|
||||
rowTableId: string
|
||||
fieldName: string
|
||||
}
|
||||
|
||||
export default class RelationshipFetch extends DataFetch<
|
||||
RelationshipDatasource,
|
||||
Table
|
||||
> {
|
||||
async getDefinition() {
|
||||
const { datasource } = this.options
|
||||
|
||||
if (!datasource?.tableId) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
return await this.API.fetchTableDefinition(datasource.tableId)
|
||||
} catch (error: any) {
|
||||
this.store.update(state => ({
|
||||
...state,
|
||||
error,
|
||||
}))
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async getData() {
|
||||
const { datasource } = this.options
|
||||
if (!datasource?.rowId || !datasource?.rowTableId) {
|
||||
return { rows: [] }
|
||||
}
|
||||
try {
|
||||
const res = await this.API.fetchRelationshipData(
|
||||
datasource.rowTableId,
|
||||
datasource.rowId,
|
||||
datasource.fieldName
|
||||
)
|
||||
return { rows: res }
|
||||
} catch (error) {
|
||||
return { rows: [] }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,14 @@
|
|||
import { get } from "svelte/store"
|
||||
import DataFetch from "./DataFetch.js"
|
||||
import { SortOrder } from "@budibase/types"
|
||||
import DataFetch from "./DataFetch"
|
||||
import { SortOrder, Table } from "@budibase/types"
|
||||
|
||||
export default class TableFetch extends DataFetch {
|
||||
determineFeatureFlags() {
|
||||
interface TableDatasource {
|
||||
type: "table"
|
||||
tableId: string
|
||||
}
|
||||
|
||||
export default class TableFetch extends DataFetch<TableDatasource, Table> {
|
||||
async determineFeatureFlags() {
|
||||
return {
|
||||
supportsSearch: true,
|
||||
supportsSort: true,
|
||||
|
@ -11,6 +16,23 @@ export default class TableFetch extends DataFetch {
|
|||
}
|
||||
}
|
||||
|
||||
async getDefinition() {
|
||||
const { datasource } = this.options
|
||||
|
||||
if (!datasource?.tableId) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
return await this.API.fetchTableDefinition(datasource.tableId)
|
||||
} catch (error: any) {
|
||||
this.store.update(state => ({
|
||||
...state,
|
||||
error,
|
||||
}))
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async getData() {
|
||||
const { datasource, limit, sortColumn, sortOrder, sortType, paginate } =
|
||||
this.options
|
||||
|
@ -23,7 +45,7 @@ export default class TableFetch extends DataFetch {
|
|||
query,
|
||||
limit,
|
||||
sort: sortColumn,
|
||||
sortOrder: sortOrder?.toLowerCase() ?? SortOrder.ASCENDING,
|
||||
sortOrder: sortOrder ?? SortOrder.ASCENDING,
|
||||
sortType,
|
||||
paginate,
|
||||
bookmark: cursor,
|
|
@ -1,19 +1,37 @@
|
|||
import { get } from "svelte/store"
|
||||
import DataFetch from "./DataFetch.js"
|
||||
import DataFetch, { DataFetchParams } from "./DataFetch"
|
||||
import { TableNames } from "../constants"
|
||||
import { utils } from "@budibase/shared-core"
|
||||
import { SearchFilters, SearchUsersRequest } from "@budibase/types"
|
||||
|
||||
export default class UserFetch extends DataFetch {
|
||||
constructor(opts) {
|
||||
interface UserFetchQuery {
|
||||
appId: string
|
||||
paginated: boolean
|
||||
}
|
||||
|
||||
interface UserDatasource {
|
||||
type: "user"
|
||||
tableId: TableNames.USERS
|
||||
}
|
||||
|
||||
interface UserDefinition {}
|
||||
|
||||
export default class UserFetch extends DataFetch<
|
||||
UserDatasource,
|
||||
UserDefinition,
|
||||
UserFetchQuery
|
||||
> {
|
||||
constructor(opts: DataFetchParams<UserDatasource, UserFetchQuery>) {
|
||||
super({
|
||||
...opts,
|
||||
datasource: {
|
||||
type: "user",
|
||||
tableId: TableNames.USERS,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
determineFeatureFlags() {
|
||||
async determineFeatureFlags() {
|
||||
return {
|
||||
supportsSearch: true,
|
||||
supportsSort: false,
|
||||
|
@ -22,9 +40,7 @@ export default class UserFetch extends DataFetch {
|
|||
}
|
||||
|
||||
async getDefinition() {
|
||||
return {
|
||||
schema: {},
|
||||
}
|
||||
return { schema: {} }
|
||||
}
|
||||
|
||||
async getData() {
|
||||
|
@ -32,15 +48,16 @@ export default class UserFetch extends DataFetch {
|
|||
const { cursor, query } = get(this.store)
|
||||
|
||||
// Convert old format to new one - we now allow use of the lucene format
|
||||
const { appId, paginated, ...rest } = query || {}
|
||||
const finalQuery = utils.isSupportedUserSearch(rest)
|
||||
? query
|
||||
: { string: { email: null } }
|
||||
const { appId, paginated, ...rest } = query
|
||||
|
||||
const finalQuery: SearchFilters = utils.isSupportedUserSearch(rest)
|
||||
? rest
|
||||
: {}
|
||||
|
||||
try {
|
||||
const opts = {
|
||||
bookmark: cursor,
|
||||
query: finalQuery,
|
||||
const opts: SearchUsersRequest = {
|
||||
bookmark: cursor ?? undefined,
|
||||
query: finalQuery ?? undefined,
|
||||
appId: appId,
|
||||
paginate: paginated || paginate,
|
||||
limit,
|
|
@ -1,17 +0,0 @@
|
|||
import DataFetch from "./DataFetch.js"
|
||||
|
||||
export default class ViewFetch extends DataFetch {
|
||||
getSchema(datasource, definition) {
|
||||
return definition?.views?.[datasource.name]?.schema
|
||||
}
|
||||
|
||||
async getData() {
|
||||
const { datasource } = this.options
|
||||
try {
|
||||
const res = await this.API.fetchViewData(datasource.name)
|
||||
return { rows: res || [] }
|
||||
} catch (error) {
|
||||
return { rows: [] }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
import { Table } from "@budibase/types"
|
||||
import DataFetch from "./DataFetch"
|
||||
|
||||
type ViewV1Datasource = {
|
||||
type: "view"
|
||||
name: string
|
||||
tableId: string
|
||||
calculation: string
|
||||
field: string
|
||||
groupBy: string
|
||||
}
|
||||
|
||||
export default class ViewFetch extends DataFetch<ViewV1Datasource, Table> {
|
||||
async getDefinition() {
|
||||
const { datasource } = this.options
|
||||
|
||||
if (!datasource?.tableId) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
return await this.API.fetchTableDefinition(datasource.tableId)
|
||||
} catch (error: any) {
|
||||
this.store.update(state => ({
|
||||
...state,
|
||||
error,
|
||||
}))
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
getSchema(definition: Table) {
|
||||
const { datasource } = this.options
|
||||
return definition?.views?.[datasource.name]?.schema
|
||||
}
|
||||
|
||||
async getData() {
|
||||
const { datasource } = this.options
|
||||
try {
|
||||
const res = await this.API.fetchViewData(datasource.name, {
|
||||
calculation: datasource.calculation,
|
||||
field: datasource.field,
|
||||
groupBy: datasource.groupBy,
|
||||
tableId: datasource.tableId,
|
||||
})
|
||||
return { rows: res || [] }
|
||||
} catch (error) {
|
||||
console.error(error, { datasource })
|
||||
return { rows: [] }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,18 @@
|
|||
import { ViewV2Type } from "@budibase/types"
|
||||
import DataFetch from "./DataFetch.js"
|
||||
import { SortOrder, ViewV2Enriched, ViewV2Type } from "@budibase/types"
|
||||
import DataFetch from "./DataFetch"
|
||||
import { get } from "svelte/store"
|
||||
import { helpers } from "@budibase/shared-core"
|
||||
|
||||
export default class ViewV2Fetch extends DataFetch {
|
||||
determineFeatureFlags() {
|
||||
interface ViewDatasource {
|
||||
type: "viewV2"
|
||||
id: string
|
||||
}
|
||||
|
||||
export default class ViewV2Fetch extends DataFetch<
|
||||
ViewDatasource,
|
||||
ViewV2Enriched
|
||||
> {
|
||||
async determineFeatureFlags() {
|
||||
return {
|
||||
supportsSearch: true,
|
||||
supportsSort: true,
|
||||
|
@ -11,18 +20,13 @@ export default class ViewV2Fetch extends DataFetch {
|
|||
}
|
||||
}
|
||||
|
||||
getSchema(datasource, definition) {
|
||||
return definition?.schema
|
||||
}
|
||||
async getDefinition() {
|
||||
const { datasource } = this.options
|
||||
|
||||
async getDefinition(datasource) {
|
||||
if (!datasource?.id) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const res = await this.API.viewV2.fetchDefinition(datasource.id)
|
||||
return res?.data
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
this.store.update(state => ({
|
||||
...state,
|
||||
error,
|
||||
|
@ -42,8 +46,10 @@ export default class ViewV2Fetch extends DataFetch {
|
|||
|
||||
// If this is a calculation view and we have no calculations, return nothing
|
||||
if (
|
||||
definition.type === ViewV2Type.CALCULATION &&
|
||||
!Object.values(definition.schema || {}).some(x => x.calculationType)
|
||||
definition?.type === ViewV2Type.CALCULATION &&
|
||||
!Object.values(definition.schema || {}).some(
|
||||
helpers.views.isCalculationField
|
||||
)
|
||||
) {
|
||||
return {
|
||||
rows: [],
|
||||
|
@ -56,26 +62,42 @@ export default class ViewV2Fetch extends DataFetch {
|
|||
// If sort/filter params are not defined, update options to store the
|
||||
// params built in to this view. This ensures that we can accurately
|
||||
// compare old and new params and skip a redundant API call.
|
||||
if (!sortColumn && definition.sort?.field) {
|
||||
if (!sortColumn && definition?.sort?.field) {
|
||||
this.options.sortColumn = definition.sort.field
|
||||
this.options.sortOrder = definition.sort.order
|
||||
this.options.sortOrder = definition.sort.order || SortOrder.ASCENDING
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await this.API.viewV2.fetch(datasource.id, {
|
||||
...(query ? { query } : {}),
|
||||
const request = {
|
||||
query,
|
||||
paginate,
|
||||
limit,
|
||||
bookmark: cursor,
|
||||
sort: sortColumn,
|
||||
sortOrder: sortOrder?.toLowerCase(),
|
||||
sortOrder: sortOrder,
|
||||
sortType,
|
||||
}
|
||||
if (paginate) {
|
||||
const res = await this.API.viewV2.fetch(datasource.id, {
|
||||
...request,
|
||||
paginate,
|
||||
})
|
||||
return {
|
||||
rows: res?.rows || [],
|
||||
hasNextPage: res?.hasNextPage || false,
|
||||
cursor: res?.bookmark || null,
|
||||
}
|
||||
} else {
|
||||
const res = await this.API.viewV2.fetch(datasource.id, {
|
||||
...request,
|
||||
paginate,
|
||||
})
|
||||
return {
|
||||
rows: res?.rows || [],
|
||||
hasNextPage: false,
|
||||
cursor: null,
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
rows: [],
|
|
@ -1,57 +0,0 @@
|
|||
import TableFetch from "./TableFetch.js"
|
||||
import ViewFetch from "./ViewFetch.js"
|
||||
import ViewV2Fetch from "./ViewV2Fetch.js"
|
||||
import QueryFetch from "./QueryFetch.js"
|
||||
import RelationshipFetch from "./RelationshipFetch.js"
|
||||
import NestedProviderFetch from "./NestedProviderFetch.js"
|
||||
import FieldFetch from "./FieldFetch.js"
|
||||
import JSONArrayFetch from "./JSONArrayFetch.js"
|
||||
import UserFetch from "./UserFetch.js"
|
||||
import GroupUserFetch from "./GroupUserFetch.js"
|
||||
import CustomFetch from "./CustomFetch.js"
|
||||
import QueryArrayFetch from "./QueryArrayFetch.js"
|
||||
|
||||
const DataFetchMap = {
|
||||
table: TableFetch,
|
||||
view: ViewFetch,
|
||||
viewV2: ViewV2Fetch,
|
||||
query: QueryFetch,
|
||||
link: RelationshipFetch,
|
||||
user: UserFetch,
|
||||
groupUser: GroupUserFetch,
|
||||
custom: CustomFetch,
|
||||
|
||||
// Client specific datasource types
|
||||
provider: NestedProviderFetch,
|
||||
field: FieldFetch,
|
||||
jsonarray: JSONArrayFetch,
|
||||
queryarray: QueryArrayFetch,
|
||||
}
|
||||
|
||||
// Constructs a new fetch model for a certain datasource
|
||||
export const fetchData = ({ API, datasource, options }) => {
|
||||
const Fetch = DataFetchMap[datasource?.type] || TableFetch
|
||||
return new Fetch({ API, datasource, ...options })
|
||||
}
|
||||
|
||||
// Creates an empty fetch instance with no datasource configured, so no data
|
||||
// will initially be loaded
|
||||
const createEmptyFetchInstance = ({ API, datasource }) => {
|
||||
const handler = DataFetchMap[datasource?.type]
|
||||
if (!handler) {
|
||||
return null
|
||||
}
|
||||
return new handler({ API })
|
||||
}
|
||||
|
||||
// Fetches the definition of any type of datasource
|
||||
export const getDatasourceDefinition = async ({ API, datasource }) => {
|
||||
const instance = createEmptyFetchInstance({ API, datasource })
|
||||
return await instance?.getDefinition(datasource)
|
||||
}
|
||||
|
||||
// Fetches the schema of any type of datasource
|
||||
export const getDatasourceSchema = ({ API, datasource, definition }) => {
|
||||
const instance = createEmptyFetchInstance({ API, datasource })
|
||||
return instance?.getSchema(datasource, definition)
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
import TableFetch from "./TableFetch"
|
||||
import ViewFetch from "./ViewFetch"
|
||||
import ViewV2Fetch from "./ViewV2Fetch"
|
||||
import QueryFetch from "./QueryFetch"
|
||||
import RelationshipFetch from "./RelationshipFetch"
|
||||
import NestedProviderFetch from "./NestedProviderFetch"
|
||||
import FieldFetch from "./FieldFetch"
|
||||
import JSONArrayFetch from "./JSONArrayFetch"
|
||||
import UserFetch from "./UserFetch"
|
||||
import GroupUserFetch from "./GroupUserFetch"
|
||||
import CustomFetch from "./CustomFetch"
|
||||
import QueryArrayFetch from "./QueryArrayFetch"
|
||||
import { APIClient } from "../api/types"
|
||||
|
||||
export type DataFetchType = keyof typeof DataFetchMap
|
||||
|
||||
export const DataFetchMap = {
|
||||
table: TableFetch,
|
||||
view: ViewFetch,
|
||||
viewV2: ViewV2Fetch,
|
||||
query: QueryFetch,
|
||||
link: RelationshipFetch,
|
||||
user: UserFetch,
|
||||
groupUser: GroupUserFetch,
|
||||
custom: CustomFetch,
|
||||
|
||||
// Client specific datasource types
|
||||
provider: NestedProviderFetch,
|
||||
field: FieldFetch<"field">,
|
||||
jsonarray: JSONArrayFetch,
|
||||
queryarray: QueryArrayFetch,
|
||||
}
|
||||
|
||||
// Constructs a new fetch model for a certain datasource
|
||||
export const fetchData = ({ API, datasource, options }: any) => {
|
||||
const Fetch = DataFetchMap[datasource?.type as DataFetchType] || TableFetch
|
||||
const fetch = new Fetch({ API, datasource, ...options })
|
||||
|
||||
// Initially fetch data but don't bother waiting for the result
|
||||
fetch.getInitialData()
|
||||
|
||||
return fetch
|
||||
}
|
||||
|
||||
// Creates an empty fetch instance with no datasource configured, so no data
|
||||
// will initially be loaded
|
||||
const createEmptyFetchInstance = <TDatasource extends { type: DataFetchType }>({
|
||||
API,
|
||||
datasource,
|
||||
}: {
|
||||
API: APIClient
|
||||
datasource: TDatasource
|
||||
}) => {
|
||||
const handler = DataFetchMap[datasource?.type as DataFetchType]
|
||||
if (!handler) {
|
||||
return null
|
||||
}
|
||||
return new handler({
|
||||
API,
|
||||
datasource: null as never,
|
||||
query: null as any,
|
||||
})
|
||||
}
|
||||
|
||||
// Fetches the definition of any type of datasource
|
||||
export const getDatasourceDefinition = async <
|
||||
TDatasource extends { type: DataFetchType }
|
||||
>({
|
||||
API,
|
||||
datasource,
|
||||
}: {
|
||||
API: APIClient
|
||||
datasource: TDatasource
|
||||
}) => {
|
||||
const instance = createEmptyFetchInstance({ API, datasource })
|
||||
return await instance?.getDefinition()
|
||||
}
|
||||
|
||||
// Fetches the schema of any type of datasource
|
||||
export const getDatasourceSchema = <
|
||||
TDatasource extends { type: DataFetchType }
|
||||
>({
|
||||
API,
|
||||
datasource,
|
||||
definition,
|
||||
}: {
|
||||
API: APIClient
|
||||
datasource: TDatasource
|
||||
definition?: any
|
||||
}) => {
|
||||
const instance = createEmptyFetchInstance({ API, datasource })
|
||||
return instance?.getSchema(definition)
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
export { createAPIClient } from "./api"
|
||||
export { fetchData } from "./fetch"
|
||||
export { fetchData, DataFetchMap } from "./fetch"
|
||||
export type { DataFetchType } from "./fetch"
|
||||
export * as Constants from "./constants"
|
||||
export * from "./stores"
|
||||
export * from "./utils"
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import { JsonFieldMetadata, QuerySchema } from "@budibase/types"
|
||||
|
||||
type Schema = Record<string, QuerySchema | string>
|
||||
|
||||
declare module "./json" {
|
||||
export const getJSONArrayDatasourceSchema: (
|
||||
tableSchema: Schema,
|
||||
datasource: any
|
||||
) => Record<string, { type: string; name: string; prefixKeys: string }>
|
||||
|
||||
export const generateQueryArraySchemas: (
|
||||
schema: Schema,
|
||||
nestedSchemaFields?: Record<string, Schema>
|
||||
) => Schema
|
||||
|
||||
export const convertJSONSchemaToTableSchema: (
|
||||
jsonSchema: JsonFieldMetadata,
|
||||
options: {
|
||||
squashObjects?: boolean
|
||||
prefixKeys?: string
|
||||
}
|
||||
) => Record<string, { type: string; name: string; prefixKeys: string }>
|
||||
}
|
|
@ -209,6 +209,9 @@ export const buildFormBlockButtonConfig = props => {
|
|||
{
|
||||
"##eventHandlerType": "Close Side Panel",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Modal",
|
||||
},
|
||||
|
||||
...(actionUrl
|
||||
? [
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
import { io } from "socket.io-client"
|
||||
import { io, Socket } from "socket.io-client"
|
||||
import { SocketEvent, SocketSessionTTL } from "@budibase/shared-core"
|
||||
import { APISessionID } from "../api"
|
||||
|
||||
const DefaultOptions = {
|
||||
heartbeat: true,
|
||||
}
|
||||
export interface ExtendedSocket extends Socket {
|
||||
onOther: (event: string, callback: (data: any) => void) => void
|
||||
}
|
||||
|
||||
export const createWebsocket = (path, options = DefaultOptions) => {
|
||||
export const createWebsocket = (
|
||||
path: string,
|
||||
options = DefaultOptions
|
||||
): ExtendedSocket => {
|
||||
if (!path) {
|
||||
throw "A websocket path must be provided"
|
||||
}
|
||||
|
@ -32,10 +38,10 @@ export const createWebsocket = (path, options = DefaultOptions) => {
|
|||
// Disable polling and rely on websocket only, as HTTP transport
|
||||
// will only work with sticky sessions which we don't have
|
||||
transports: ["websocket"],
|
||||
})
|
||||
}) as ExtendedSocket
|
||||
|
||||
// Set up a heartbeat that's half of the session TTL
|
||||
let interval
|
||||
let interval: NodeJS.Timeout | undefined
|
||||
if (heartbeat) {
|
||||
interval = setInterval(() => {
|
||||
socket.emit(SocketEvent.Heartbeat)
|
||||
|
@ -48,7 +54,7 @@ export const createWebsocket = (path, options = DefaultOptions) => {
|
|||
|
||||
// Helper utility to ignore events that were triggered due to API
|
||||
// changes made by us
|
||||
socket.onOther = (event, callback) => {
|
||||
socket.onOther = (event: string, callback: (data: any) => void) => {
|
||||
socket.on(event, data => {
|
||||
if (data?.apiSessionId !== APISessionID) {
|
||||
callback(data)
|
|
@ -1 +1 @@
|
|||
Subproject commit ae786121d923449b0ad5fcbd123d0a9fec28f65e
|
||||
Subproject commit 193476cdfade6d3c613e6972f16ee0c527e01ff6
|
|
@ -355,7 +355,7 @@ async function execute(
|
|||
ExecuteQueryRequest,
|
||||
ExecuteV2QueryResponse | ExecuteV1QueryResponse
|
||||
>,
|
||||
opts: any = { rowsOnly: false, isAutomation: false }
|
||||
opts = { rowsOnly: false, isAutomation: false }
|
||||
) {
|
||||
const db = context.getAppDB()
|
||||
|
||||
|
@ -416,7 +416,7 @@ export async function executeV1(
|
|||
export async function executeV2(
|
||||
ctx: UserCtx<ExecuteQueryRequest, ExecuteV2QueryResponse>
|
||||
) {
|
||||
return execute(ctx, { rowsOnly: false })
|
||||
return execute(ctx, { rowsOnly: false, isAutomation: false })
|
||||
}
|
||||
|
||||
export async function executeV2AsAutomation(
|
||||
|
|
|
@ -4,15 +4,8 @@ import {
|
|||
processAIColumns,
|
||||
processFormulas,
|
||||
} from "../../../utilities/rowProcessor"
|
||||
import { context, features } from "@budibase/backend-core"
|
||||
import {
|
||||
Table,
|
||||
Row,
|
||||
FeatureFlag,
|
||||
FormulaType,
|
||||
FieldType,
|
||||
ViewV2,
|
||||
} from "@budibase/types"
|
||||
import { context } from "@budibase/backend-core"
|
||||
import { Table, Row, FormulaType, FieldType, ViewV2 } from "@budibase/types"
|
||||
import * as linkRows from "../../../db/linkedRows"
|
||||
import isEqual from "lodash/isEqual"
|
||||
import { cloneDeep, merge } from "lodash/fp"
|
||||
|
@ -162,11 +155,10 @@ export async function finaliseRow(
|
|||
dynamic: false,
|
||||
contextRows: [enrichedRow],
|
||||
})
|
||||
|
||||
const aiEnabled =
|
||||
((await features.flags.isEnabled(FeatureFlag.BUDIBASE_AI)) &&
|
||||
(await pro.features.isBudibaseAIEnabled())) ||
|
||||
((await features.flags.isEnabled(FeatureFlag.AI_CUSTOM_CONFIGS)) &&
|
||||
(await pro.features.isAICustomConfigsEnabled()))
|
||||
(await pro.features.isBudibaseAIEnabled()) ||
|
||||
(await pro.features.isAICustomConfigsEnabled())
|
||||
if (aiEnabled) {
|
||||
row = await processAIColumns(table, row, {
|
||||
contextRows: [enrichedRow],
|
||||
|
@ -184,11 +176,6 @@ export async function finaliseRow(
|
|||
enrichedRow = await processFormulas(table, enrichedRow, {
|
||||
dynamic: false,
|
||||
})
|
||||
if (aiEnabled) {
|
||||
enrichedRow = await processAIColumns(table, enrichedRow, {
|
||||
contextRows: [enrichedRow],
|
||||
})
|
||||
}
|
||||
|
||||
// this updates the related formulas in other rows based on the relations to this row
|
||||
if (updateFormula) {
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import {
|
||||
UserCtx,
|
||||
ViewV2,
|
||||
SearchRowResponse,
|
||||
SearchViewRowRequest,
|
||||
RequiredKeys,
|
||||
RowSearchParams,
|
||||
PaginatedSearchRowResponse,
|
||||
} from "@budibase/types"
|
||||
import sdk from "../../../sdk"
|
||||
import { context } from "@budibase/backend-core"
|
||||
|
||||
export async function searchView(
|
||||
ctx: UserCtx<SearchViewRowRequest, SearchRowResponse>
|
||||
ctx: UserCtx<SearchViewRowRequest, PaginatedSearchRowResponse>
|
||||
) {
|
||||
const { viewId } = ctx.params
|
||||
|
||||
|
@ -49,7 +49,13 @@ export async function searchView(
|
|||
user: sdk.users.getUserContextBindings(ctx.user),
|
||||
})
|
||||
result.rows.forEach(r => (r._viewId = view.id))
|
||||
ctx.body = result
|
||||
|
||||
ctx.body = {
|
||||
rows: result.rows,
|
||||
bookmark: result.bookmark,
|
||||
hasNextPage: result.hasNextPage,
|
||||
totalRows: result.totalRows,
|
||||
}
|
||||
}
|
||||
|
||||
function getSortOptions(request: SearchViewRowRequest, view: ViewV2) {
|
||||
|
|
|
@ -8,7 +8,13 @@ import {
|
|||
import tk from "timekeeper"
|
||||
import emitter from "../../../../src/events"
|
||||
import { outputProcessing } from "../../../utilities/rowProcessor"
|
||||
import { context, InternalTable, tenancy, utils } from "@budibase/backend-core"
|
||||
import {
|
||||
context,
|
||||
setEnv,
|
||||
InternalTable,
|
||||
tenancy,
|
||||
utils,
|
||||
} from "@budibase/backend-core"
|
||||
import { quotas } from "@budibase/pro"
|
||||
import {
|
||||
AIOperationEnum,
|
||||
|
@ -42,19 +48,8 @@ import { InternalTables } from "../../../db/utils"
|
|||
import { withEnv } from "../../../environment"
|
||||
import { JsTimeoutError } from "@budibase/string-templates"
|
||||
import { isDate } from "../../../utilities"
|
||||
|
||||
jest.mock("@budibase/pro", () => ({
|
||||
...jest.requireActual("@budibase/pro"),
|
||||
ai: {
|
||||
LargeLanguageModel: {
|
||||
forCurrentTenant: async () => ({
|
||||
llm: {},
|
||||
run: jest.fn(() => `Mock LLM Response`),
|
||||
buildPromptFromAIOperation: jest.fn(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
}))
|
||||
import nock from "nock"
|
||||
import { mockChatGPTResponse } from "../../../tests/utilities/mocks/openai"
|
||||
|
||||
const timestamp = new Date("2023-01-26T11:48:57.597Z").toISOString()
|
||||
tk.freeze(timestamp)
|
||||
|
@ -99,6 +94,8 @@ if (descriptions.length) {
|
|||
const ds = await dsProvider()
|
||||
datasource = ds.datasource
|
||||
client = ds.client
|
||||
|
||||
mocks.licenses.useCloudFree()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
|
@ -172,10 +169,6 @@ if (descriptions.length) {
|
|||
)
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
mocks.licenses.useCloudFree()
|
||||
})
|
||||
|
||||
const getRowUsage = async () => {
|
||||
const { total } = await config.doInContext(undefined, () =>
|
||||
quotas.getCurrentUsageValues(
|
||||
|
@ -3224,10 +3217,17 @@ if (descriptions.length) {
|
|||
isInternal &&
|
||||
describe("AI fields", () => {
|
||||
let table: Table
|
||||
let envCleanup: () => void
|
||||
|
||||
beforeAll(async () => {
|
||||
mocks.licenses.useBudibaseAI()
|
||||
mocks.licenses.useAICustomConfigs()
|
||||
envCleanup = setEnv({
|
||||
OPENAI_API_KEY: "sk-abcdefghijklmnopqrstuvwxyz1234567890abcd",
|
||||
})
|
||||
|
||||
mockChatGPTResponse("Mock LLM Response")
|
||||
|
||||
table = await config.api.table.save(
|
||||
saveTableRequest({
|
||||
schema: {
|
||||
|
@ -3251,7 +3251,9 @@ if (descriptions.length) {
|
|||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.unmock("@budibase/pro")
|
||||
nock.cleanAll()
|
||||
envCleanup()
|
||||
mocks.licenses.useCloudFree()
|
||||
})
|
||||
|
||||
it("should be able to save a row with an AI column", async () => {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
AIOperationEnum,
|
||||
ArrayOperator,
|
||||
BasicOperator,
|
||||
BBReferenceFieldSubType,
|
||||
|
@ -42,7 +43,9 @@ import {
|
|||
} from "../../../integrations/tests/utils"
|
||||
import merge from "lodash/merge"
|
||||
import { quotas } from "@budibase/pro"
|
||||
import { context, db, events, roles } from "@budibase/backend-core"
|
||||
import { context, db, events, roles, setEnv } from "@budibase/backend-core"
|
||||
import { mockChatGPTResponse } from "../../../tests/utilities/mocks/openai"
|
||||
import nock from "nock"
|
||||
|
||||
const descriptions = datasourceDescribe({ exclude: [DatabaseName.MONGODB] })
|
||||
|
||||
|
@ -100,6 +103,7 @@ if (descriptions.length) {
|
|||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
mocks.licenses.useCloudFree()
|
||||
|
||||
const ds = await dsProvider()
|
||||
rawDatasource = ds.rawDatasource
|
||||
|
@ -109,7 +113,6 @@ if (descriptions.length) {
|
|||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
mocks.licenses.useCloudFree()
|
||||
})
|
||||
|
||||
describe("view crud", () => {
|
||||
|
@ -507,7 +510,6 @@ if (descriptions.length) {
|
|||
})
|
||||
|
||||
it("readonly fields can be used on free license", async () => {
|
||||
mocks.licenses.useCloudFree()
|
||||
const table = await config.api.table.save(
|
||||
saveTableRequest({
|
||||
schema: {
|
||||
|
@ -933,6 +935,95 @@ if (descriptions.length) {
|
|||
}
|
||||
)
|
||||
})
|
||||
|
||||
isInternal &&
|
||||
describe("AI fields", () => {
|
||||
let envCleanup: () => void
|
||||
beforeAll(() => {
|
||||
mocks.licenses.useBudibaseAI()
|
||||
mocks.licenses.useAICustomConfigs()
|
||||
envCleanup = setEnv({
|
||||
OPENAI_API_KEY: "sk-abcdefghijklmnopqrstuvwxyz1234567890abcd",
|
||||
})
|
||||
|
||||
mockChatGPTResponse(prompt => {
|
||||
if (prompt.includes("elephant")) {
|
||||
return "big"
|
||||
}
|
||||
if (prompt.includes("mouse")) {
|
||||
return "small"
|
||||
}
|
||||
if (prompt.includes("whale")) {
|
||||
return "big"
|
||||
}
|
||||
return "unknown"
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
nock.cleanAll()
|
||||
envCleanup()
|
||||
mocks.licenses.useCloudFree()
|
||||
})
|
||||
|
||||
it("can use AI fields in view calculations", async () => {
|
||||
const table = await config.api.table.save(
|
||||
saveTableRequest({
|
||||
schema: {
|
||||
animal: {
|
||||
name: "animal",
|
||||
type: FieldType.STRING,
|
||||
},
|
||||
bigOrSmall: {
|
||||
name: "bigOrSmall",
|
||||
type: FieldType.AI,
|
||||
operation: AIOperationEnum.CATEGORISE_TEXT,
|
||||
categories: "big,small",
|
||||
columns: ["animal"],
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
const view = await config.api.viewV2.create({
|
||||
tableId: table._id!,
|
||||
name: generator.guid(),
|
||||
type: ViewV2Type.CALCULATION,
|
||||
schema: {
|
||||
bigOrSmall: {
|
||||
visible: true,
|
||||
},
|
||||
count: {
|
||||
visible: true,
|
||||
calculationType: CalculationType.COUNT,
|
||||
field: "animal",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await config.api.row.save(table._id!, {
|
||||
animal: "elephant",
|
||||
})
|
||||
|
||||
await config.api.row.save(table._id!, {
|
||||
animal: "mouse",
|
||||
})
|
||||
|
||||
await config.api.row.save(table._id!, {
|
||||
animal: "whale",
|
||||
})
|
||||
|
||||
const { rows } = await config.api.row.search(view.id, {
|
||||
sort: "bigOrSmall",
|
||||
sortOrder: SortOrder.ASCENDING,
|
||||
})
|
||||
expect(rows).toHaveLength(2)
|
||||
expect(rows[0].bigOrSmall).toEqual("big")
|
||||
expect(rows[1].bigOrSmall).toEqual("small")
|
||||
expect(rows[0].count).toEqual(2)
|
||||
expect(rows[1].count).toEqual(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("update", () => {
|
||||
|
@ -1836,7 +1927,6 @@ if (descriptions.length) {
|
|||
},
|
||||
})
|
||||
|
||||
mocks.licenses.useCloudFree()
|
||||
const view = await getDelegate(res)
|
||||
expect(view.schema?.one).toEqual(
|
||||
expect.objectContaining({ visible: true, readonly: true })
|
||||
|
|
|
@ -27,11 +27,9 @@ import {
|
|||
Hosting,
|
||||
ActionImplementation,
|
||||
AutomationStepDefinition,
|
||||
FeatureFlag,
|
||||
} from "@budibase/types"
|
||||
import sdk from "../sdk"
|
||||
import { getAutomationPlugin } from "../utilities/fileSystem"
|
||||
import { features } from "@budibase/backend-core"
|
||||
|
||||
type ActionImplType = ActionImplementations<
|
||||
typeof env.SELF_HOSTED extends "true" ? Hosting.SELF : Hosting.CLOUD
|
||||
|
@ -78,6 +76,7 @@ export const BUILTIN_ACTION_DEFINITIONS: Record<
|
|||
LOOP: loop.definition,
|
||||
COLLECT: collect.definition,
|
||||
TRIGGER_AUTOMATION_RUN: triggerAutomationRun.definition,
|
||||
BRANCH: branch.definition,
|
||||
// these used to be lowercase step IDs, maintain for backwards compat
|
||||
discord: discord.definition,
|
||||
slack: slack.definition,
|
||||
|
@ -105,14 +104,7 @@ if (env.SELF_HOSTED) {
|
|||
export async function getActionDefinitions(): Promise<
|
||||
Record<keyof typeof AutomationActionStepId, AutomationStepDefinition>
|
||||
> {
|
||||
if (await features.flags.isEnabled(FeatureFlag.AUTOMATION_BRANCHING)) {
|
||||
BUILTIN_ACTION_DEFINITIONS["BRANCH"] = branch.definition
|
||||
}
|
||||
if (
|
||||
env.SELF_HOSTED ||
|
||||
(await features.flags.isEnabled(FeatureFlag.BUDIBASE_AI)) ||
|
||||
(await features.flags.isEnabled(FeatureFlag.AI_CUSTOM_CONFIGS))
|
||||
) {
|
||||
if (env.SELF_HOSTED) {
|
||||
BUILTIN_ACTION_DEFINITIONS["OPENAI"] = openai.definition
|
||||
}
|
||||
|
||||
|
|
|
@ -7,9 +7,8 @@ import {
|
|||
AutomationIOType,
|
||||
OpenAIStepInputs,
|
||||
OpenAIStepOutputs,
|
||||
FeatureFlag,
|
||||
} from "@budibase/types"
|
||||
import { env, features } from "@budibase/backend-core"
|
||||
import { env } from "@budibase/backend-core"
|
||||
import * as automationUtils from "../automationUtils"
|
||||
import * as pro from "@budibase/pro"
|
||||
|
||||
|
@ -99,12 +98,8 @@ export async function run({
|
|||
|
||||
try {
|
||||
let response
|
||||
const customConfigsEnabled =
|
||||
(await features.flags.isEnabled(FeatureFlag.AI_CUSTOM_CONFIGS)) &&
|
||||
(await pro.features.isAICustomConfigsEnabled())
|
||||
const budibaseAIEnabled =
|
||||
(await features.flags.isEnabled(FeatureFlag.BUDIBASE_AI)) &&
|
||||
(await pro.features.isBudibaseAIEnabled())
|
||||
const customConfigsEnabled = await pro.features.isAICustomConfigsEnabled()
|
||||
const budibaseAIEnabled = await pro.features.isBudibaseAIEnabled()
|
||||
|
||||
let llmWrapper
|
||||
if (budibaseAIEnabled || customConfigsEnabled) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -432,6 +432,21 @@ export async function enrichSchema(
|
|||
...tableSchema[key],
|
||||
...ui,
|
||||
order: anyViewOrder ? ui?.order ?? undefined : tableSchema[key]?.order,
|
||||
// When this was written, the only column types in FieldSchema to have columns
|
||||
// field were the relationship columns. We blank this out here to make sure it's
|
||||
// not set on non-relationship columns, then below we populate it by calling
|
||||
// populateRelSchema.
|
||||
//
|
||||
// For Budibase 3.0 we introduced the FieldType.AI fields. Some of these fields
|
||||
// have `columns: string[]` and it flew under the radar here because the
|
||||
// AIFieldMetadata type isn't a union on its subtypes, it has a collection of
|
||||
// optional fields. So columns is `columns?: string[]` which allows undefined,
|
||||
// and doesn't fail this type check.
|
||||
//
|
||||
// What this means in practice is when FieldType.AI fields get enriched, we
|
||||
// delete their `columns`. At the time of writing, I don't believe anything in
|
||||
// the frontend depends on this, but it is odd and will probably bite us at
|
||||
// some point.
|
||||
columns: undefined,
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import nock from "nock"
|
||||
|
||||
let chatID = 1
|
||||
|
||||
export function mockChatGPTResponse(
|
||||
response: string | ((prompt: string) => string)
|
||||
) {
|
||||
return nock("https://api.openai.com")
|
||||
.post("/v1/chat/completions")
|
||||
.reply(200, (uri, requestBody) => {
|
||||
let content = response
|
||||
if (typeof response === "function") {
|
||||
const messages = (requestBody as any).messages
|
||||
content = response(messages[0].content)
|
||||
}
|
||||
|
||||
chatID++
|
||||
|
||||
return {
|
||||
id: `chatcmpl-${chatID}`,
|
||||
object: "chat.completion",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: "gpt-4o-mini",
|
||||
system_fingerprint: `fp_${chatID}`,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message: { role: "assistant", content },
|
||||
logprobs: null,
|
||||
finish_reason: "stop",
|
||||
},
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
completion_tokens_details: {
|
||||
reasoning_tokens: 0,
|
||||
accepted_prediction_tokens: 0,
|
||||
rejected_prediction_tokens: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
.persist()
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue