Merge branch 'master' of github.com:Budibase/budibase into feature/sql-attachments

This commit is contained in:
mike12345567 2025-03-13 11:57:41 +00:00
commit 1c41fee91a
35 changed files with 601 additions and 147 deletions

View File

@ -1,32 +1,34 @@
<script lang="ts">
import "@spectrum-css/textfield/dist/index-vars.css"
import { createEventDispatcher } from "svelte"
import type { FocusEventHandler } from "svelte/elements"
export let value = ""
export let placeholder: string | undefined = undefined
export let value: string | null = null
export let placeholder: string | null = null
export let disabled = false
export let readonly = false
export let id: string | undefined = undefined
export let height: string | number | undefined = undefined
export let minHeight: string | number | undefined = undefined
export const getCaretPosition = () => ({
start: textarea.selectionStart,
end: textarea.selectionEnd,
})
export let id: string | null = null
export let height: number | null = null
export let minHeight: number | null = null
export let align = null
let focus = false
let textarea: any
const dispatch = createEventDispatcher()
const onChange = (event: any) => {
dispatch("change", event.target.value)
focus = false
let isFocused = false
let textarea: HTMLTextAreaElement
const dispatch = createEventDispatcher<{ change: string }>()
const onChange: FocusEventHandler<HTMLTextAreaElement> = event => {
dispatch("change", event.currentTarget.value)
isFocused = false
}
const getStyleString = (
attribute: string,
value: string | number | undefined
) => {
export function focus() {
textarea.focus()
}
export function contents() {
return textarea.value
}
const getStyleString = (attribute: string, value: number | null) => {
if (!attribute || value == null) {
return ""
}
@ -44,7 +46,7 @@
style={`${heightString}${minHeightString}`}
class="spectrum-Textfield spectrum-Textfield--multiline"
class:is-disabled={disabled}
class:is-focused={focus}
class:is-focused={isFocused}
>
<!-- prettier-ignore -->
<textarea
@ -55,8 +57,9 @@
{disabled}
{readonly}
{id}
on:focus={() => (focus = true)}
on:focus={() => (isFocused = true)}
on:blur={onChange}
on:keypress
>{value || ""}</textarea>
</div>

View File

@ -5,17 +5,25 @@
export let value: string | undefined = undefined
export let label: string | undefined = undefined
export let labelPosition: string = "above"
export let labelPosition = "above"
export let placeholder: string | undefined = undefined
export let disabled = false
export let error: string | undefined = undefined
export let getCaretPosition: any = undefined
export let height: string | number | undefined = undefined
export let minHeight: string | number | undefined = undefined
export let height: number | undefined = undefined
export let minHeight: number | undefined = undefined
export let helpText: string | undefined = undefined
let textarea: TextArea
export function focus() {
textarea.focus()
}
export function contents() {
return textarea.contents()
}
const dispatch = createEventDispatcher()
const onChange = (e: any) => {
const onChange = (e: CustomEvent<string>) => {
value = e.detail
dispatch("change", e.detail)
}
@ -23,12 +31,13 @@
<Field {helpText} {label} {labelPosition} {error}>
<TextArea
bind:getCaretPosition
bind:this={textarea}
{disabled}
{value}
{placeholder}
{height}
{minHeight}
on:change={onChange}
on:keypress
/>
</Field>

View File

@ -56,7 +56,10 @@
memo,
fetchData,
} from "@budibase/frontend-core"
import { getSchemaForDatasourcePlus } from "@/dataBinding"
import {
getSchemaForDatasourcePlus,
readableToRuntimeBinding,
} from "@/dataBinding"
import { TriggerStepID, ActionStepID } from "@/constants/backend/automations"
import { onMount, createEventDispatcher } from "svelte"
import { writable } from "svelte/store"
@ -1034,7 +1037,10 @@
{bindings}
{schema}
panel={AutomationBindingPanel}
on:change={e => onChange({ [key]: e.detail })}
on:change={e =>
onChange({
[key]: readableToRuntimeBinding(bindings, e.detail),
})}
context={$memoContext}
value={inputData[key]}
/>

View File

@ -6,7 +6,13 @@
</script>
<script lang="ts">
import { Label } from "@budibase/bbui"
import {
Button,
Label,
notifications,
Popover,
TextArea,
} from "@budibase/bbui"
import { onMount, createEventDispatcher, onDestroy } from "svelte"
import { FIND_ANY_HBS_REGEX } from "@budibase/string-templates"
@ -53,11 +59,14 @@
import { javascript } from "@codemirror/lang-javascript"
import { EditorModes } from "./"
import { themeStore } from "@/stores/portal"
import type { EditorMode } from "@budibase/types"
import { FeatureFlag, type EditorMode } from "@budibase/types"
import { tooltips } from "@codemirror/view"
import type { BindingCompletion, CodeValidator } from "@/types"
import { validateHbsTemplate } from "./validator/hbs"
import { validateJsTemplate } from "./validator/js"
import { featureFlag } from "@/helpers"
import { API } from "@/api"
import Spinner from "../Spinner.svelte"
export let label: string | undefined = undefined
export let completions: BindingCompletion[] = []
@ -86,6 +95,13 @@
let isDark = !currentTheme.includes("light")
let themeConfig = new Compartment()
let popoverAnchor: HTMLElement
let popover: Popover
let promptInput: TextArea
$: aiGenEnabled =
featureFlag.isEnabled(FeatureFlag.AI_JS_GENERATION) &&
mode.name === "javascript"
$: {
if (autofocus && isEditorInitialised) {
editor.focus()
@ -139,6 +155,57 @@
}
}
$: promptLoading = false
let popoverWidth = 300
let suggestedCode: string | null = null
let previousContents: string | null = null
const generateJs = async (prompt: string) => {
previousContents = editor.state.doc.toString()
promptLoading = true
popoverWidth = 30
let code = ""
try {
const resp = await API.generateJs({ prompt })
code = resp.code
} catch (e) {
console.error(e)
notifications.error("Unable to generate code, please try again later.")
code = previousContents
popoverWidth = 300
promptLoading = false
popover.hide()
return
}
value = code
editor.dispatch({
changes: { from: 0, to: editor.state.doc.length, insert: code },
})
suggestedCode = code
popoverWidth = 100
promptLoading = false
}
const acceptSuggestion = () => {
suggestedCode = null
previousContents = null
resetPopover()
}
const rejectSuggestion = () => {
suggestedCode = null
value = previousContents || ""
editor.dispatch({
changes: { from: 0, to: editor.state.doc.length, insert: value },
})
previousContents = null
resetPopover()
}
const resetPopover = () => {
popover.hide()
popoverWidth = 300
}
// Export a function to expose caret position
export const getCaretPosition = () => {
const selection_range = editor.state.selection.ranges[0]
@ -428,6 +495,50 @@
<div tabindex="-1" bind:this={textarea} />
</div>
{#if aiGenEnabled}
<button
bind:this={popoverAnchor}
class="ai-gen"
on:click={() => {
popover.show()
setTimeout(() => {
promptInput.focus()
}, 100)
}}
>
Generate with AI ✨
</button>
<Popover
bind:this={popover}
minWidth={popoverWidth}
anchor={popoverAnchor}
align="left-outside"
>
{#if promptLoading}
<div class="prompt-spinner">
<Spinner size="20" color="white" />
</div>
{:else if suggestedCode}
<Button on:click={acceptSuggestion}>Accept</Button>
<Button on:click={rejectSuggestion}>Reject</Button>
{:else}
<TextArea
bind:this={promptInput}
placeholder="Type your prompt then press enter..."
on:keypress={event => {
if (event.getModifierState("Shift")) {
return
}
if (event.key === "Enter") {
generateJs(promptInput.contents())
}
}}
/>
{/if}
</Popover>
{/if}
<style>
/* Editor */
.code-editor {
@ -633,4 +744,34 @@
text-overflow: ellipsis !important;
white-space: nowrap !important;
}
.ai-gen {
right: 1px;
bottom: 1px;
position: absolute;
justify-content: center;
align-items: center;
display: flex;
flex-direction: row;
box-sizing: border-box;
padding: var(--spacing-s);
border-left: 1px solid var(--spectrum-alias-border-color);
border-top: 1px solid var(--spectrum-alias-border-color);
border-top-left-radius: var(--spectrum-alias-border-radius-regular);
color: var(--spectrum-global-color-blue-700);
background-color: var(--spectrum-global-color-gray-75);
transition: background-color
var(--spectrum-global-animation-duration-100, 130ms),
box-shadow var(--spectrum-global-animation-duration-100, 130ms),
border-color var(--spectrum-global-animation-duration-100, 130ms);
height: calc(var(--spectrum-alias-item-height-m) - 2px);
}
.ai-gen:hover {
cursor: pointer;
color: var(--spectrum-alias-text-color-hover);
background-color: var(--spectrum-global-color-gray-50);
border-color: var(--spectrum-alias-border-color-hover);
}
.prompt-spinner {
padding: var(--spacing-m);
}
</style>

View File

@ -2,10 +2,11 @@
import { Circle } from "svelte-loading-spinners"
export let size = "60"
export let color = "var(--ink)"
</script>
<div class="spinner-container">
<Circle {size} color="var(--ink)" unit="px" />
<Circle {size} {color} unit="px" />
</div>
<style>

View File

@ -37,7 +37,7 @@ const { ContextScopes } = Constants
// Regex to match all instances of template strings
const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
const CAPTURE_VAR_INSIDE_JS = /\$\("([^")]+)"\)/g
const CAPTURE_VAR_INSIDE_JS = /\$\((["'`])([^"'`]+)\1\)/g
const CAPTURE_HBS_TEMPLATE = /{{[\S\s]*?}}/g
const UpdateReferenceAction = {

View File

@ -1,31 +1,36 @@
<script>
<script lang="ts">
import { sdk } from "@budibase/shared-core"
import { FieldType } from "@budibase/types"
import RelationshipField from "./RelationshipField.svelte"
export let defaultValue
export let defaultValue: string
export let type = FieldType.BB_REFERENCE
function updateUserIDs(value) {
function updateUserIDs(value: string | string[]) {
if (Array.isArray(value)) {
return value.map(val => sdk.users.getGlobalUserID(val))
return value.map(val => sdk.users.getGlobalUserID(val)!)
} else {
return sdk.users.getGlobalUserID(value)
}
}
function updateReferences(value) {
function updateReferences(value: string) {
if (sdk.users.containsUserID(value)) {
return updateUserIDs(value)
}
return value
}
$: updatedDefaultValue = updateReferences(defaultValue)
// This cannot be typed, as svelte does not provide typed inheritance
$: allProps = $$props as any
</script>
<RelationshipField
{...$$props}
{...allProps}
{type}
datasourceType={"user"}
primaryDisplay={"email"}
defaultValue={updateReferences(defaultValue)}
defaultValue={updatedDefaultValue}
/>

View File

@ -1,43 +1,60 @@
<script lang="ts">
import { getContext, onDestroy } from "svelte"
import type { Readable } from "svelte/store"
import { writable } from "svelte/store"
import { Icon } from "@budibase/bbui"
import { memo } from "@budibase/frontend-core"
import Placeholder from "../Placeholder.svelte"
import InnerForm from "./InnerForm.svelte"
import type { FieldApi } from "."
import type { FieldSchema, FieldType } from "@budibase/types"
import type {
FieldApi,
FieldState,
FieldValidation,
FormField,
} from "@/types"
interface FieldInfo {
field: string
type: FieldType
defaultValue: string | undefined
disabled: boolean
readonly: boolean
validation?: FieldValidation
formStep: number
}
export let label: string | undefined = undefined
export let field: string | undefined = undefined
export let fieldState: any
export let fieldApi: FieldApi
export let fieldSchema: any
export let fieldState: FieldState | undefined
export let fieldApi: FieldApi | undefined
export let fieldSchema: FieldSchema | undefined
export let defaultValue: string | undefined = undefined
export let type: any
export let type: FieldType
export let disabled = false
export let readonly = false
export let validation: any
export let validation: FieldValidation | undefined
export let span = 6
export let helpText: string | undefined = undefined
// Get contexts
const formContext: any = getContext("form")
const formStepContext: any = getContext("form-step")
const fieldGroupContext: any = getContext("field-group")
const formContext = getContext("form")
const formStepContext = getContext("form-step")
const fieldGroupContext = getContext("field-group")
const { styleable, builderStore, Provider } = getContext("sdk")
const component: any = getContext("component")
const component = getContext("component")
// Register field with form
const formApi = formContext?.formApi
const labelPos = fieldGroupContext?.labelPosition || "above"
let formField: any
let formField: Readable<FormField> | undefined
let touched = false
let labelNode: any
let labelNode: HTMLElement | undefined
// Memoize values required to register the field to avoid loops
const formStep = formStepContext || writable(1)
const fieldInfo = memo({
const fieldInfo = memo<FieldInfo>({
field: field || $component.name,
type,
defaultValue,
@ -66,16 +83,22 @@
$: $component.editing && labelNode?.focus()
// Update form properties in parent component on every store change
$: unsubscribe = formField?.subscribe((value: any) => {
fieldState = value?.fieldState
fieldApi = value?.fieldApi
fieldSchema = value?.fieldSchema
})
$: unsubscribe = formField?.subscribe(
(value?: {
fieldState: FieldState
fieldApi: FieldApi
fieldSchema: FieldSchema
}) => {
fieldState = value?.fieldState
fieldApi = value?.fieldApi
fieldSchema = value?.fieldSchema
}
)
// Determine label class from position
$: labelClass = labelPos === "above" ? "" : `spectrum-FieldLabel--${labelPos}`
const registerField = (info: any) => {
const registerField = (info: FieldInfo) => {
formField = formApi?.registerField(
info.field,
info.type,
@ -87,10 +110,10 @@
)
}
const updateLabel = (e: any) => {
const updateLabel = (e: Event) => {
if (touched) {
// @ts-expect-error and TODO updateProp isn't recognised - need builder TS conversion
builderStore.actions.updateProp("label", e.target.textContent)
const label = e.target as HTMLLabelElement
builderStore.actions.updateProp("label", label.textContent)
}
touched = false
}

View File

@ -9,17 +9,19 @@
RelationshipFieldMetadata,
Row,
} from "@budibase/types"
import type { FieldApi, FieldState } from "."
import type { FieldApi, FieldState, FieldValidation } from "@/types"
type ValueType = string | string[]
export let field: string | undefined = undefined
export let label: string | undefined = undefined
export let placeholder: string | undefined = undefined
export let disabled: boolean = false
export let readonly: boolean = false
export let validation: any
export let validation: FieldValidation | undefined = undefined
export let autocomplete: boolean = true
export let defaultValue: string | string[] | undefined = undefined
export let onChange: any
export let defaultValue: ValueType | undefined = undefined
export let onChange: (_props: { value: ValueType }) => void
export let filter: SearchFilter[]
export let datasourceType: "table" | "user" = "table"
export let primaryDisplay: string | undefined = undefined
@ -88,14 +90,14 @@
// Ensure backwards compatibility
$: enrichedDefaultValue = enrichDefaultValue(defaultValue)
$: emptyValue = multiselect ? [] : undefined
// We need to cast value to pass it down, as those components aren't typed
$: emptyValue = multiselect ? [] : null
$: displayValue = missingIDs.length ? emptyValue : (selectedValue as any)
$: displayValue = (missingIDs.length ? emptyValue : selectedValue) as any
// Ensures that we flatten any objects so that only the IDs of the selected
// rows are passed down. Not sure how this can be an object to begin with?
const parseSelectedValue = (
value: any,
value: ValueType | undefined,
multiselect: boolean
): undefined | string | string[] => {
return multiselect ? flatten(value) : flatten(value)[0]
@ -140,7 +142,7 @@
// Builds a map of all available options, in a consistent structure
const processOptions = (
realValue: any | any[],
realValue: ValueType | undefined,
rows: Row[],
primaryDisplay?: string
) => {
@ -171,7 +173,7 @@
// Parses a row-like structure into a properly shaped option
const parseOption = (
option: any | BasicRelatedRow | Row,
option: string | BasicRelatedRow | Row,
primaryDisplay?: string
): BasicRelatedRow | null => {
if (!option || typeof option !== "object" || !option?._id) {

View File

@ -19,15 +19,3 @@ export { default as codescanner } from "./CodeScannerField.svelte"
export { default as signaturesinglefield } from "./SignatureField.svelte"
export { default as bbreferencefield } from "./BBReferenceField.svelte"
export { default as bbreferencesinglefield } from "./BBReferenceSingleField.svelte"
export interface FieldApi {
setValue(value: any): boolean
deregister(): void
}
export interface FieldState<T> {
value: T
fieldId: string
disabled: boolean
readonly: boolean
}

View File

@ -1,7 +1,12 @@
import { Component, Context, SDK } from "."
import { Writable } from "svelte"
import { Component, FieldGroupContext, FormContext, SDK } from "@/types"
import { Readable } from "svelte/store"
declare module "svelte" {
export function getContext(key: "sdk"): SDK
export function getContext(key: "component"): Component
export function getContext(key: "context"): Context
export function getContext(key: "context"): Readable<Record<string, any>>
export function getContext(key: "form"): FormContext | undefined
export function getContext(key: "form-step"): Writable<number> | undefined
export function getContext(key: "field-group"): FieldGroupContext | undefined
}

View File

@ -14,8 +14,6 @@ import {
} from "@/stores"
import { get } from "svelte/store"
import { initWebsocket } from "@/websocket"
import { APIClient } from "@budibase/frontend-core"
import type { ActionTypes } from "@/constants"
import { Readable } from "svelte/store"
import {
Screen,
@ -39,6 +37,7 @@ window.svelte = svelte
// Initialise spectrum icons
// eslint-disable-next-line local-rules/no-budibase-imports
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-vite.js"
loadSpectrumIcons()
// Extend global window scope
@ -73,32 +72,6 @@ declare global {
}
}
export interface SDK {
API: APIClient
styleable: any
Provider: any
ActionTypes: typeof ActionTypes
fetchDatasourceSchema: any
generateGoldenSample: any
builderStore: Readable<{
inBuilder: boolean
}> & {
actions: {
highlightSetting: (key: string) => void
addParentComponent: (
componentId: string,
fullAncestorType: string
) => void
}
}
}
export type Component = Readable<{
id: string
styles: any
errorState: boolean
}>
export type Context = Readable<Record<string, any>>
let app: ClientApp

View File

@ -0,0 +1,9 @@
import { Readable } from "svelte/store"
export type Component = Readable<{
id: string
name: string
styles: any
editing: boolean
errorState: boolean
}>

View File

@ -0,0 +1,3 @@
export interface FieldGroupContext {
labelPosition: string
}

View File

@ -0,0 +1,37 @@
import { Readable } from "svelte/store"
import { FieldSchema, FieldType } from "@budibase/types"
export interface FormContext {
formApi?: {
registerField: (
field: string,
type: FieldType,
defaultValue: string | undefined,
disabled: boolean,
readonly: boolean,
validation: FieldValidation | undefined,
formStep: number
) => Readable<FormField>
}
}
export type FieldValidation = () => string | undefined
export interface FormField {
fieldState: FieldState
fieldApi: FieldApi
fieldSchema: FieldSchema
}
export interface FieldApi {
setValue(value: any): boolean
deregister(): void
}
export interface FieldState<T = any> {
value: T
fieldId: string
disabled: boolean
readonly: boolean
error?: string
}

View File

@ -0,0 +1,4 @@
export * from "./components"
export * from "./fields"
export * from "./forms"
export * from "./sdk"

View File

@ -0,0 +1,24 @@
import { ActionTypes } from "@/constants"
import { APIClient } from "@budibase/frontend-core"
import { Readable } from "svelte/store"
export interface SDK {
API: APIClient
styleable: any
Provider: any
ActionTypes: typeof ActionTypes
fetchDatasourceSchema: any
generateGoldenSample: any
builderStore: Readable<{
inBuilder: boolean
}> & {
actions: {
highlightSetting: (key: string) => void
addParentComponent: (
componentId: string,
fullAncestorType: string
) => void
updateProp: (key: string, value: any) => void
}
}
}

View File

@ -1,7 +1,9 @@
import { GenerateJsRequest, GenerateJsResponse } from "@budibase/types"
import { BaseAPIClient } from "./types"
export interface AIEndpoints {
generateCronExpression: (prompt: string) => Promise<{ message: string }>
generateJs: (req: GenerateJsRequest) => Promise<GenerateJsResponse>
}
export const buildAIEndpoints = (API: BaseAPIClient): AIEndpoints => ({
@ -14,4 +16,11 @@ export const buildAIEndpoints = (API: BaseAPIClient): AIEndpoints => ({
body: { prompt },
})
},
generateJs: async req => {
return await API.post({
url: "/api/ai/js",
body: req,
})
},
})

@ -1 +1 @@
Subproject commit 2dd06c2fcb3cf10d5f16f5d8fe6cd344c8e905a5
Subproject commit bb1ed6fa96ebed30e30659e47b0712567601f3c0

View File

@ -0,0 +1,28 @@
import {
CreateOAuth2ConfigRequest,
Ctx,
FetchOAuth2ConfigsResponse,
OAuth2Config,
RequiredKeys,
} from "@budibase/types"
import sdk from "../../sdk"
export async function fetch(ctx: Ctx<void, FetchOAuth2ConfigsResponse>) {
const configs = await sdk.oauth2.fetch()
const response: FetchOAuth2ConfigsResponse = {
configs: (configs || []).map(c => ({
name: c.name,
})),
}
ctx.body = response
}
export async function create(ctx: Ctx<CreateOAuth2ConfigRequest, void>) {
const newConfig: RequiredKeys<OAuth2Config> = {
name: ctx.request.body.name,
}
await sdk.oauth2.create(newConfig)
ctx.status = 201
}

View File

@ -29,11 +29,12 @@ import debugRoutes from "./debug"
import Router from "@koa/router"
import { api as pro } from "@budibase/pro"
import rowActionRoutes from "./rowAction"
import oauth2Routes from "./oauth2"
export { default as staticRoutes } from "./static"
export { default as publicRoutes } from "./public"
const aiRoutes = pro.ai
const proAiRoutes = pro.ai
const appBackupRoutes = pro.appBackups
const environmentVariableRoutes = pro.environmentVariables
@ -68,7 +69,8 @@ export const mainRoutes: Router[] = [
debugRoutes,
environmentVariableRoutes,
rowActionRoutes,
aiRoutes,
proAiRoutes,
oauth2Routes,
// these need to be handled last as they still use /api/:tableId
// this could be breaking as koa may recognise other routes as this
tableRoutes,

View File

@ -0,0 +1,16 @@
import Router from "@koa/router"
import { PermissionType } from "@budibase/types"
import authorized from "../../middleware/authorized"
import * as controller from "../controllers/oauth2"
const router: Router = new Router()
router.get("/api/oauth2", authorized(PermissionType.BUILDER), controller.fetch)
router.post(
"/api/oauth2",
authorized(PermissionType.BUILDER),
controller.create
)
export default router

View File

@ -0,0 +1,79 @@
import { CreateOAuth2ConfigRequest } from "@budibase/types"
import * as setup from "./utilities"
import { generator } from "@budibase/backend-core/tests"
describe("/oauth2", () => {
let config = setup.getConfig()
function makeOAuth2Config(): CreateOAuth2ConfigRequest {
return {
name: generator.guid(),
}
}
beforeAll(async () => await config.init())
beforeEach(async () => await config.newTenant())
describe("fetch", () => {
it("returns empty when no oauth are created", async () => {
const response = await config.api.oauth2.fetch()
expect(response).toEqual({
configs: [],
})
})
})
describe("create", () => {
it("can create a new configuration", async () => {
const oauth2Config = makeOAuth2Config()
await config.api.oauth2.create(oauth2Config, { status: 201 })
const response = await config.api.oauth2.fetch()
expect(response).toEqual({
configs: [
{
name: oauth2Config.name,
},
],
})
})
it("can create multiple configurations", async () => {
const oauth2Config = makeOAuth2Config()
const oauth2Config2 = makeOAuth2Config()
await config.api.oauth2.create(oauth2Config, { status: 201 })
await config.api.oauth2.create(oauth2Config2, { status: 201 })
const response = await config.api.oauth2.fetch()
expect(response.configs).toEqual([
{
name: oauth2Config.name,
},
{
name: oauth2Config2.name,
},
])
})
it("cannot create configurations with already existing names", async () => {
const oauth2Config = makeOAuth2Config()
const oauth2Config2 = { ...makeOAuth2Config(), name: oauth2Config.name }
await config.api.oauth2.create(oauth2Config, { status: 201 })
await config.api.oauth2.create(oauth2Config2, {
status: 400,
body: {
message: "Name already used",
status: 400,
},
})
const response = await config.api.oauth2.fetch()
expect(response.configs).toEqual([
{
name: oauth2Config.name,
},
])
})
})
})

View File

@ -0,0 +1,28 @@
import { context, HTTPError } from "@budibase/backend-core"
import { DocumentType, OAuth2Config, OAuth2Configs } from "@budibase/types"
export async function fetch(): Promise<OAuth2Config[]> {
const db = context.getAppDB()
const result = await db.tryGet<OAuth2Configs>(DocumentType.OAUTH2_CONFIG)
if (!result) {
return []
}
return Object.values(result.configs)
}
export async function create(config: OAuth2Config) {
const db = context.getAppDB()
const doc: OAuth2Configs = (await db.tryGet<OAuth2Configs>(
DocumentType.OAUTH2_CONFIG
)) ?? {
_id: DocumentType.OAUTH2_CONFIG,
configs: {},
}
if (doc.configs[config.name]) {
throw new HTTPError("Name already used", 400)
}
doc.configs[config.name] = config
await db.put(doc)
}

View File

@ -13,6 +13,7 @@ import * as permissions from "./app/permissions"
import * as rowActions from "./app/rowActions"
import * as screens from "./app/screens"
import * as common from "./app/common"
import * as oauth2 from "./app/oauth2"
const sdk = {
backups,
@ -30,6 +31,7 @@ const sdk = {
links,
rowActions,
common,
oauth2,
}
// default export for TS

View File

@ -20,6 +20,7 @@ import { WebhookAPI } from "./webhook"
import { EnvironmentAPI } from "./environment"
import { UserPublicAPI } from "./public/user"
import { MiscAPI } from "./misc"
import { OAuth2API } from "./oauth2"
export default class API {
application: ApplicationAPI
@ -30,6 +31,7 @@ export default class API {
environment: EnvironmentAPI
legacyView: LegacyViewAPI
misc: MiscAPI
oauth2: OAuth2API
permission: PermissionAPI
plugin: PluginAPI
query: QueryAPI
@ -56,6 +58,7 @@ export default class API {
this.environment = new EnvironmentAPI(config)
this.legacyView = new LegacyViewAPI(config)
this.misc = new MiscAPI(config)
this.oauth2 = new OAuth2API(config)
this.permission = new PermissionAPI(config)
this.plugin = new PluginAPI(config)
this.query = new QueryAPI(config)

View File

@ -0,0 +1,23 @@
import {
CreateOAuth2ConfigRequest,
FetchOAuth2ConfigsResponse,
} from "@budibase/types"
import { Expectations, TestAPI } from "./base"
export class OAuth2API extends TestAPI {
fetch = async (expectations?: Expectations) => {
return await this._get<FetchOAuth2ConfigsResponse>("/api/oauth2", {
expectations,
})
}
create = async (
body: CreateOAuth2ConfigRequest,
expectations?: Expectations
) => {
return await this._post<CreateOAuth2ConfigRequest>("/api/oauth2", {
body,
expectations,
})
}
}

View File

@ -0,0 +1,6 @@
export interface GenerateJsRequest {
prompt: string
}
export interface GenerateJsResponse {
code: string
}

View File

@ -1,21 +1,22 @@
export * from "./backup"
export * from "./datasource"
export * from "./view"
export * from "./rows"
export * from "./table"
export * from "./permission"
export * from "./attachment"
export * from "./user"
export * from "./rowAction"
export * from "./automation"
export * from "./component"
export * from "./integration"
export * from "./metadata"
export * from "./query"
export * from "./screen"
export * from "./application"
export * from "./layout"
export * from "./attachment"
export * from "./automation"
export * from "./backup"
export * from "./component"
export * from "./datasource"
export * from "./deployment"
export * from "./integration"
export * from "./layout"
export * from "./metadata"
export * from "./oauth2"
export * from "./permission"
export * from "./query"
export * from "./role"
export * from "./webhook"
export * from "./rowAction"
export * from "./rows"
export * from "./screen"
export * from "./static"
export * from "./table"
export * from "./user"
export * from "./view"
export * from "./webhook"

View File

@ -0,0 +1,9 @@
interface OAuth2Config {
name: string
}
export interface FetchOAuth2ConfigsResponse {
configs: OAuth2Config[]
}
export interface CreateOAuth2ConfigRequest extends OAuth2Config {}

View File

@ -1,3 +1,4 @@
export * from "./ai"
export * from "./analytics"
export * from "./auth"
export * from "./user"

View File

@ -1,22 +1,23 @@
export * from "../document"
export * from "./app"
export * from "./automation"
export * from "./backup"
export * from "./component"
export * from "./datasource"
export * from "./deployment"
export * from "./layout"
export * from "./links"
export * from "./metadata"
export * from "./oauth2"
export * from "./query"
export * from "./role"
export * from "./table"
export * from "./screen"
export * from "./view"
export * from "../document"
export * from "./row"
export * from "./user"
export * from "./backup"
export * from "./webhook"
export * from "./links"
export * from "./component"
export * from "./sqlite"
export * from "./snippet"
export * from "./rowAction"
export * from "./screen"
export * from "./snippet"
export * from "./sqlite"
export * from "./table"
export * from "./theme"
export * from "./deployment"
export * from "./metadata"
export * from "./user"
export * from "./view"
export * from "./webhook"

View File

@ -0,0 +1,9 @@
import { Document } from "../document"
export interface OAuth2Config {
name: string
}
export interface OAuth2Configs extends Document {
configs: Record<string, OAuth2Config>
}

View File

@ -40,6 +40,7 @@ export enum DocumentType {
APP_MIGRATION_METADATA = "_design/migrations",
SCIM_LOG = "scimlog",
ROW_ACTIONS = "ra",
OAUTH2_CONFIG = "oauth2",
}
// Because DocumentTypes can overlap, we need to make sure that we search

View File

@ -3,10 +3,13 @@ export enum FeatureFlag {
// Account-portal
DIRECT_LOGIN_TO_ACCOUNT_PORTAL = "DIRECT_LOGIN_TO_ACCOUNT_PORTAL",
AI_JS_GENERATION = "AI_JS_GENERATION",
}
export const FeatureFlagDefaults = {
export const FeatureFlagDefaults: Record<FeatureFlag, boolean> = {
[FeatureFlag.USE_ZOD_VALIDATOR]: false,
[FeatureFlag.AI_JS_GENERATION]: false,
// Account-portal
[FeatureFlag.DIRECT_LOGIN_TO_ACCOUNT_PORTAL]: false,