Merge branch 'BUDI-9127/split-oauthconfig-per-document' into BUDI-9127/track-usage

This commit is contained in:
Adria Navarro 2025-03-21 09:12:56 +01:00
commit f904646519
30 changed files with 510 additions and 385 deletions

View File

@ -200,3 +200,13 @@ export function getStartEndKeyURL(baseKey: any, tenantId?: string) {
export const getPluginParams = (pluginId?: string | null, otherProps = {}) => {
return getDocParams(DocumentType.PLUGIN, pluginId, otherProps)
}
/**
* Gets parameters for retrieving OAuth2 configs, this is a utility function for the getDocParams function.
*/
export const getOAuth2ConfigParams = (
configId?: string | null,
otherProps: Partial<DatabaseQueryOpts> = {}
) => {
return getDocParams(DocumentType.OAUTH2_CONFIG, configId, otherProps)
}

View File

@ -1,4 +1,4 @@
<script>
<script lang="ts">
import Popover from "../Popover/Popover.svelte"
import Layout from "../Layout/Layout.svelte"
import { createEventDispatcher } from "svelte"
@ -11,15 +11,17 @@
getThemeClassNames,
DefaultAppTheme,
} from "@budibase/shared-core"
import type { Theme } from "@budibase/types"
import type { PopoverAlignment } from "../constants"
export let value
export let size = "M"
export let spectrumTheme
export let offset
export let align
export let value: string | undefined = undefined
export let size: "S" | "M" | "L" = "M"
export let spectrumTheme: Theme | undefined = undefined
export let offset: number | undefined = undefined
export let align: PopoverAlignment | undefined = undefined
let dropdown
let preview
let dropdown: Popover | undefined
let preview: HTMLElement | undefined
$: customValue = getCustomValue(value)
$: checkColor = getCheckColor(value)
@ -124,7 +126,7 @@
},
]
const getThemeClasses = theme => {
const getThemeClasses = (theme: Theme | undefined) => {
if (!theme) {
return ""
}
@ -132,12 +134,12 @@
return getThemeClassNames(theme)
}
const onChange = value => {
const onChange = (value: string | null) => {
dispatch("change", value)
dropdown.hide()
dropdown?.hide()
}
const getCustomValue = value => {
const getCustomValue = (value: string | undefined) => {
if (!value) {
return value
}
@ -152,11 +154,11 @@
return found ? null : value
}
const prettyPrint = color => {
const prettyPrint = (color: string) => {
return capitalise(color.split("-").join(" "))
}
const getCheckColor = value => {
const getCheckColor = (value: string | undefined) => {
// Use dynamic color for theme grays
if (value?.includes("-gray-")) {
return /^.*(gray-(50|75|100|200|300|400|500))\)$/.test(value)
@ -187,7 +189,7 @@
bind:this={preview}
class="preview size--{size || 'M'}"
on:click={() => {
dropdown.toggle()
dropdown?.toggle()
}}
>
<div

View File

@ -88,16 +88,14 @@
// Required for reactivity
fields = fields
const newActivity = {}
const trimmedFields = []
for (let idx = 0; idx < fields.length; idx++) {
const fieldName = fields[idx].name
if (fieldName) {
newActivity[fieldName] = fieldActivity[idx]
trimmedFields.push(fields[idx])
}
}
activity = newActivity
dispatch("change", trimmedFields)
dispatch("change", fields)
}
function isJsonArray(value) {

View File

@ -29,7 +29,7 @@
...authConfigs,
...$oauth2.configs.map(c => ({
label: c.name,
value: c.id,
value: c._id,
})),
]
$: authConfig = allConfigs.find(c => c.value === authConfigId)
@ -108,8 +108,9 @@
{#each $oauth2.configs as config}
<ListItem
title={config.name}
on:click={() => selectConfiguration(config.id, RestAuthType.OAUTH2)}
selected={config.id === authConfigId}
on:click={() =>
selectConfiguration(config._id, RestAuthType.OAUTH2)}
selected={config._id === authConfigId}
/>
{/each}
</List>

View File

@ -7,7 +7,7 @@
Modal,
notifications,
} from "@budibase/bbui"
import type { OAuth2Config } from "@budibase/types"
import type { OAuth2Config } from "@/types"
import OAuth2ConfigModalContent from "./OAuth2ConfigModalContent.svelte"
import { confirm } from "@/helpers"
@ -26,7 +26,7 @@
warning: true,
onConfirm: async () => {
try {
await oauth2.delete(row.id)
await oauth2.delete(row._id, row._rev)
notifications.success(`Config '${row.name}' deleted successfully`)
} catch (e: any) {
let message = "Error deleting config"

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { oauth2 } from "@/stores/builder"
import type { OAuth2Config, UpsertOAuth2Config } from "@/types"
import type { OAuth2Config } from "@/types"
import {
Body,
Divider,
@ -12,6 +12,7 @@
notifications,
Select,
} from "@budibase/bbui"
import type { InsertOAuth2ConfigRequest } from "@budibase/types"
import {
OAuth2CredentialsMethod,
PASSWORD_REPLACEMENT,
@ -50,7 +51,7 @@
name: requiredString("Name is required.").refine(
val =>
!$oauth2.configs
.filter(c => c.id !== config.id)
.filter(c => c._id !== config._id)
.map(c => c.name.toLowerCase())
.includes(val.toLowerCase()),
{
@ -63,7 +64,7 @@
method: z.nativeEnum(OAuth2CredentialsMethod, {
message: "Authentication method is required.",
}),
}) satisfies ZodType<UpsertOAuth2Config>
}) satisfies ZodType<InsertOAuth2ConfigRequest>
const validationResult = validator.safeParse(config)
errors = {}
@ -91,7 +92,7 @@
const { data: configData } = validationResult
try {
const connectionValidation = await oauth2.validate({
id: config?.id,
_id: config?._id,
url: configData.url,
clientId: configData.clientId,
clientSecret: configData.clientSecret,
@ -110,7 +111,11 @@
await oauth2.create(configData)
notifications.success("Settings created.")
} else {
await oauth2.edit(config!.id, configData)
await oauth2.edit({
...configData,
_id: config!._id,
_rev: config!._rev,
})
notifications.success("Settings saved.")
}
} catch (e: any) {

View File

@ -1,7 +1,11 @@
import { API } from "@/api"
import { BudiStore } from "@/stores/BudiStore"
import { OAuth2Config, UpsertOAuth2Config } from "@/types"
import { RequiredKeys, ValidateConfigRequest } from "@budibase/types"
import { OAuth2Config } from "@/types"
import {
InsertOAuth2ConfigRequest,
UpdateOAuth2ConfigRequest,
ValidateConfigRequest,
} from "@budibase/types"
interface OAuth2StoreState {
configs: OAuth2Config[]
@ -26,8 +30,9 @@ export class OAuth2Store extends BudiStore<OAuth2StoreState> {
const configs = await API.oauth2.fetch()
this.store.update(store => ({
...store,
configs: configs.map<RequiredKeys<OAuth2Config>>(c => ({
id: c.id,
configs: configs.map(c => ({
_id: c._id,
_rev: c._rev,
name: c.name,
url: c.url,
clientId: c.clientId,
@ -46,18 +51,18 @@ export class OAuth2Store extends BudiStore<OAuth2StoreState> {
}
}
async create(config: UpsertOAuth2Config) {
async create(config: InsertOAuth2ConfigRequest) {
await API.oauth2.create(config)
await this.fetch()
}
async edit(id: string, config: UpsertOAuth2Config) {
await API.oauth2.update(id, config)
async edit(config: UpdateOAuth2ConfigRequest) {
await API.oauth2.update(config)
await this.fetch()
}
async delete(id: string) {
await API.oauth2.delete(id)
async delete(id: string, rev: string) {
await API.oauth2.delete(id, rev)
await this.fetch()
}

View File

@ -1,8 +1,3 @@
import {
UpsertOAuth2ConfigRequest,
FetchOAuth2ConfigsResponse,
} from "@budibase/types"
import { OAuth2ConfigResponse } from "@budibase/types"
export type OAuth2Config = FetchOAuth2ConfigsResponse["configs"][0]
export interface UpsertOAuth2Config extends UpsertOAuth2ConfigRequest {}
export interface OAuth2Config extends OAuth2ConfigResponse {}

View File

@ -1,38 +1,39 @@
<script>
<script lang="ts">
import { getContext } from "svelte"
import InnerForm from "./InnerForm.svelte"
import { Helpers } from "@budibase/bbui"
import { writable } from "svelte/store"
import type { DataFetchDatasource, Table, TableSchema } from "@budibase/types"
export let dataSource
export let size
export let disabled = false
export let readonly = false
export let actionType = "Create"
export let initialFormStep = 1
export let dataSource: DataFetchDatasource
export let size: "Medium" | "Large"
export let disabled: boolean = false
export let readonly: boolean = false
export let actionType: "Create" = "Create"
export let initialFormStep: string | number = 1
// Not exposed as a builder setting. Used internally to disable validation
// for fields rendered in things like search blocks.
export let disableSchemaValidation = false
export let disableSchemaValidation: boolean = false
// Not exposed as a builder setting. Used internally to allow searching on
// auto columns.
export let editAutoColumns = false
export let editAutoColumns: boolean = false
const context = getContext("context")
const component = getContext("component")
const { fetchDatasourceSchema, fetchDatasourceDefinition } = getContext("sdk")
const getInitialFormStep = () => {
const parsedFormStep = parseInt(initialFormStep)
const parsedFormStep = parseInt(initialFormStep.toString())
if (isNaN(parsedFormStep)) {
return 1
}
return parsedFormStep
}
let definition
let schema
let definition: Table | undefined
let schema: TableSchema | undefined
let loaded = false
let currentStep = getContext("current-step") || writable(getInitialFormStep())
@ -49,7 +50,12 @@
)
// Returns the closes data context which isn't a built in context
const getInitialValues = (type, dataSource, path, context) => {
const getInitialValues = (
type: string,
dataSource: DataFetchDatasource,
path: string[],
context: Record<string, any>
) => {
// Only inherit values for update forms
if (type !== "Update") {
return {}
@ -82,11 +88,11 @@
}
// Fetches the form schema from this form's dataSource
const fetchSchema = async dataSource => {
const fetchSchema = async (dataSource: DataFetchDatasource) => {
try {
definition = await fetchDatasourceDefinition(dataSource)
} catch (error) {
definition = null
definition = undefined
}
const res = await fetchDatasourceSchema(dataSource)
schema = res || {}
@ -98,7 +104,7 @@
// Generates a predictable string that uniquely identifies a schema. We can't
// simply stringify the whole schema as there are array fields which have
// random order.
const generateSchemaKey = schema => {
const generateSchemaKey = (schema: TableSchema | undefined) => {
if (!schema) {
return null
}

View File

@ -1,31 +1,62 @@
<script>
<script lang="ts">
import { setContext, getContext } from "svelte"
import type { Readable, Writable } from "svelte/store"
import { derived, get, writable } from "svelte/store"
import { createValidatorFromConstraints } from "./validation"
import { Helpers } from "@budibase/bbui"
import type {
DataFetchDatasource,
FieldSchema,
FieldType,
Table,
TableSchema,
UIFieldValidationRule,
} from "@budibase/types"
export let dataSource = undefined
export let disabled = false
export let readonly = false
export let initialValues = undefined
export let size = undefined
export let schema = undefined
export let definition = undefined
export let disableSchemaValidation = false
export let editAutoColumns = false
type FieldInfo<T = any> = {
name: string
step: number
type: `${FieldType}`
fieldState: {
fieldId: string
value: T
defaultValue: T
disabled: boolean
readonly: boolean
validator: ((_value: T) => string | null) | null
error: string | null | undefined
lastUpdate: number
}
fieldApi: {
setValue(_value: T): void
validate(): boolean
reset(): void
}
fieldSchema: FieldSchema | {}
}
export let dataSource: DataFetchDatasource | undefined = undefined
export let disabled: boolean = false
export let readonly: boolean = false
export let initialValues: Record<string, any> | undefined = undefined
export let size: "Medium" | "Large" | undefined = undefined
export let schema: TableSchema | undefined = undefined
export let definition: Table | undefined = undefined
export let disableSchemaValidation: boolean = false
export let editAutoColumns: boolean = false
// For internal use only, to disable context when being used with standalone
// fields
export let provideContext = true
export let provideContext: boolean = true
// We export this store so that when we remount the inner form we can still
// persist what step we're on
export let currentStep
export let currentStep: Writable<number>
const component = getContext("component")
const { styleable, Provider, ActionTypes } = getContext("sdk")
let fields = []
let fields: Writable<FieldInfo>[] = []
const formState = writable({
values: {},
errors: {},
@ -75,19 +106,24 @@
// Generates a derived store from an array of fields, comprised of a map of
// extracted values from the field array
const deriveFieldProperty = (fieldStores, getProp) => {
const deriveFieldProperty = (
fieldStores: Readable<FieldInfo>[],
getProp: (_field: FieldInfo) => any
) => {
return derived(fieldStores, fieldValues => {
const reducer = (map, field) => ({ ...map, [field.name]: getProp(field) })
return fieldValues.reduce(reducer, {})
return fieldValues.reduce(
(map, field) => ({ ...map, [field.name]: getProp(field) }),
{}
)
})
}
// Derives any enrichments which need to be made so that bindings work for
// special data types like attachments. Relationships are currently not
// handled as we don't have the primaryDisplay field that is required.
const deriveBindingEnrichments = fieldStores => {
const deriveBindingEnrichments = (fieldStores: Readable<FieldInfo>[]) => {
return derived(fieldStores, fieldValues => {
let enrichments = {}
const enrichments: Record<string, string> = {}
fieldValues.forEach(field => {
if (field.type === "attachment") {
const value = field.fieldState.value
@ -104,7 +140,11 @@
// Derive the overall form value and deeply set all field paths so that we
// can support things like JSON fields.
const deriveFormValue = (initialValues, values, enrichments) => {
const deriveFormValue = (
initialValues: Record<string, any> | undefined,
values: Record<string, any>,
enrichments: Record<string, string>
) => {
let formValue = Helpers.cloneDeep(initialValues || {})
// We need to sort the keys to avoid a JSON field overwriting a nested field
@ -118,7 +158,7 @@
}
})
.sort((a, b) => {
return a.lastUpdate > b.lastUpdate
return a.lastUpdate - b.lastUpdate
})
// Merge all values and enrichments into a single value
@ -132,12 +172,16 @@
}
// Searches the field array for a certain field
const getField = name => {
return fields.find(field => get(field).name === name)
const getField = (name: string) => {
return fields.find(field => get(field).name === name)!
}
// Sanitises a value by ensuring it doesn't contain any invalid data
const sanitiseValue = (value, schema, type) => {
const sanitiseValue = (
value: any,
schema: FieldSchema | undefined,
type: `${FieldType}`
) => {
// Check arrays - remove any values not present in the field schema and
// convert any values supplied to strings
if (Array.isArray(value) && type === "array" && schema) {
@ -149,13 +193,13 @@
const formApi = {
registerField: (
field,
type,
defaultValue = null,
fieldDisabled = false,
fieldReadOnly = false,
validationRules,
step = 1
field: string,
type: FieldType,
defaultValue: string | null = null,
fieldDisabled: boolean = false,
fieldReadOnly: boolean = false,
validationRules: UIFieldValidationRule[],
step: number = 1
) => {
if (!field) {
return
@ -200,7 +244,7 @@
const isAutoColumn = !!schema?.[field]?.autocolumn
// Construct field info
const fieldInfo = writable({
const fieldInfo = writable<FieldInfo>({
name: field,
type,
step: step || 1,
@ -210,7 +254,8 @@
error: initialError,
disabled:
disabled || fieldDisabled || (isAutoColumn && !editAutoColumns),
readonly: readonly || fieldReadOnly || schema?.[field]?.readonly,
readonly:
readonly || fieldReadOnly || (schema?.[field] as any)?.readonly,
defaultValue,
validator,
lastUpdate: Date.now(),
@ -254,7 +299,13 @@
get(field).fieldApi.reset()
})
},
changeStep: ({ type, number }) => {
changeStep: ({
type,
number,
}: {
type: "next" | "prev" | "first" | "specific"
number: any
}) => {
if (type === "next") {
currentStep.update(step => step + 1)
} else if (type === "prev") {
@ -265,12 +316,12 @@
currentStep.set(parseInt(number))
}
},
setStep: step => {
setStep: (step: number) => {
if (step) {
currentStep.set(step)
}
},
setFieldValue: (fieldName, value) => {
setFieldValue: (fieldName: string, value: any) => {
const field = getField(fieldName)
if (!field) {
return
@ -278,7 +329,7 @@
const { fieldApi } = get(field)
fieldApi.setValue(value)
},
resetField: fieldName => {
resetField: (fieldName: string) => {
const field = getField(fieldName)
if (!field) {
return
@ -289,9 +340,9 @@
}
// Creates an API for a specific field
const makeFieldApi = field => {
const makeFieldApi = (field: string) => {
// Sets the value for a certain field and invokes validation
const setValue = (value, skipCheck = false) => {
const setValue = (value: any, skipCheck = false) => {
const fieldInfo = getField(field)
const { fieldState } = get(fieldInfo)
const { validator } = fieldState
@ -328,36 +379,6 @@
})
}
// Updates the validator rules for a certain field
const updateValidation = validationRules => {
const fieldInfo = getField(field)
const { fieldState } = get(fieldInfo)
const { value, error } = fieldState
// Create new validator
const schemaConstraints = disableSchemaValidation
? null
: schema?.[field]?.constraints
const validator = createValidatorFromConstraints(
schemaConstraints,
validationRules,
field,
definition
)
// Update validator
fieldInfo.update(state => {
state.fieldState.validator = validator
return state
})
// If there is currently an error, run the validator again in case
// the error should be cleared by the new validation rules
if (error) {
setValue(value, true)
}
}
// We don't want to actually remove the field state when deregistering, just
// remove any errors and validation
const deregister = () => {
@ -370,7 +391,7 @@
}
// Updates the disabled state of a certain field
const setDisabled = fieldDisabled => {
const setDisabled = (fieldDisabled: boolean) => {
const fieldInfo = getField(field)
// Auto columns are always disabled
@ -386,7 +407,6 @@
return {
setValue,
reset,
updateValidation,
setDisabled,
deregister,
validate: () => {
@ -412,7 +432,15 @@
// register their fields to step 1
setContext("form-step", writable(1))
const handleUpdateFieldValue = ({ type, field, value }) => {
const handleUpdateFieldValue = ({
type,
field,
value,
}: {
type: "set" | "reset"
field: string
value: any
}) => {
if (type === "set") {
formApi.setFieldValue(field, value)
} else {
@ -420,16 +448,19 @@
}
}
const handleScrollToField = ({ field }) => {
if (!field.fieldState) {
field = get(getField(field))
const handleScrollToField = (props: { field: FieldInfo | string }) => {
let field
if (typeof props.field === "string") {
field = get(getField(props.field))
} else {
field = props.field
}
const fieldId = field.fieldState.fieldId
const fieldElement = document.getElementById(fieldId)
if (fieldElement) {
fieldElement.focus({ preventScroll: true })
}
const label = document.querySelector(`label[for="${fieldId}"]`)
const label = document.querySelector<HTMLElement>(`label[for="${fieldId}"]`)
if (label) {
label.style.scrollMargin = "100px"
label.scrollIntoView({ behavior: "smooth", block: "nearest" })

View File

@ -1,6 +1,11 @@
import dayjs from "dayjs"
import { FieldTypes } from "../../../constants"
import { Helpers } from "@budibase/bbui"
import {
FieldConstraints,
FieldType,
Table,
UIFieldValidationRule,
} from "@budibase/types"
/**
* Creates a validation function from a combination of schema-level constraints
@ -12,19 +17,19 @@ import { Helpers } from "@budibase/bbui"
* @returns {function} a validator function which accepts test values
*/
export const createValidatorFromConstraints = (
schemaConstraints,
customRules,
field,
definition
schemaConstraints: FieldConstraints | null | undefined,
customRules: UIFieldValidationRule[],
field: string,
definition: Table | undefined
) => {
let rules = []
let rules: UIFieldValidationRule[] = []
// Convert schema constraints into validation rules
if (schemaConstraints) {
// Required constraint
if (
field === definition?.primaryDisplay ||
schemaConstraints.presence?.allowEmpty === false ||
(schemaConstraints.presence as any)?.allowEmpty === false ||
schemaConstraints.presence === true
) {
rules.push({
@ -106,7 +111,7 @@ export const createValidatorFromConstraints = (
rules = rules.concat(customRules || [])
// Evaluate each constraint
return value => {
return (value: any) => {
for (let rule of rules) {
const error = evaluateRule(rule, value)
if (error) {
@ -124,7 +129,7 @@ export const createValidatorFromConstraints = (
* @param value the value to validate against
* @returns {null|*} an error if validation fails or null if it passes
*/
const evaluateRule = (rule, value) => {
const evaluateRule = (rule: UIFieldValidationRule, value: any) => {
if (!rule) {
return null
}
@ -150,14 +155,14 @@ const evaluateRule = (rule, value) => {
* @param type the type to parse
* @returns {boolean|string|*|number|null|array} the parsed value, or null if invalid
*/
const parseType = (value, type) => {
const parseType = (value: any, type: `${FieldType}`) => {
// Treat nulls or empty strings as null
if (!exists(value) || !type) {
return null
}
// Parse as string
if (type === FieldTypes.STRING) {
if (type === FieldType.STRING) {
if (typeof value === "string" || Array.isArray(value)) {
return value
}
@ -168,7 +173,7 @@ const parseType = (value, type) => {
}
// Parse as number
if (type === FieldTypes.NUMBER) {
if (type === FieldType.NUMBER) {
if (isNaN(value)) {
return null
}
@ -176,7 +181,7 @@ const parseType = (value, type) => {
}
// Parse as date
if (type === FieldTypes.DATETIME) {
if (type === FieldType.DATETIME) {
if (value instanceof Date) {
return value.getTime()
}
@ -185,7 +190,7 @@ const parseType = (value, type) => {
}
// Parse as boolean
if (type === FieldTypes.BOOLEAN) {
if (type === FieldType.BOOLEAN) {
if (typeof value === "string") {
return value.toLowerCase() === "true"
}
@ -193,7 +198,7 @@ const parseType = (value, type) => {
}
// Parse attachments, treating no elements as null
if (type === FieldTypes.ATTACHMENTS) {
if (type === FieldType.ATTACHMENTS) {
if (!Array.isArray(value) || !value.length) {
return null
}
@ -202,8 +207,8 @@ const parseType = (value, type) => {
// Parse attachment/signature single, treating no key as null
if (
type === FieldTypes.ATTACHMENT_SINGLE ||
type === FieldTypes.SIGNATURE_SINGLE
type === FieldType.ATTACHMENT_SINGLE ||
type === FieldType.SIGNATURE_SINGLE
) {
if (!value?.key) {
return null
@ -212,7 +217,7 @@ const parseType = (value, type) => {
}
// Parse links, treating no elements as null
if (type === FieldTypes.LINK) {
if (type === FieldType.LINK) {
if (!Array.isArray(value) || !value.length) {
return null
}
@ -220,7 +225,7 @@ const parseType = (value, type) => {
}
// Parse array, treating no elements as null
if (type === FieldTypes.ARRAY) {
if (type === FieldType.ARRAY) {
if (!Array.isArray(value) || !value.length) {
return null
}
@ -229,7 +234,7 @@ const parseType = (value, type) => {
// For JSON we don't touch the value at all as we want to verify it in its
// raw form
if (type === FieldTypes.JSON) {
if (type === FieldType.JSON) {
return value
}
@ -238,69 +243,74 @@ const parseType = (value, type) => {
}
// Evaluates a required constraint
const requiredHandler = value => {
const requiredHandler = (value: any) => {
return value != null
}
// Evaluates a min length constraint
const minLengthHandler = (value, rule) => {
const minLengthHandler = (value: any, rule: UIFieldValidationRule) => {
const limit = parseType(rule.value, "number")
return value == null || value.length >= limit
}
// Evaluates a max length constraint
const maxLengthHandler = (value, rule) => {
const maxLengthHandler = (value: any, rule: UIFieldValidationRule) => {
const limit = parseType(rule.value, "number")
return value == null || value.length <= limit
}
// Evaluates a max file size (MB) constraint
const maxFileSizeHandler = (value, rule) => {
const maxFileSizeHandler = (value: any, rule: UIFieldValidationRule) => {
const limit = parseType(rule.value, "number")
const check = attachment => attachment.size / 1000000 > limit
const check = (attachment: { size: number }) =>
attachment.size / 1000000 > limit
return value == null || !(value?.key ? check(value) : value.some(check))
}
// Evaluates a max total upload size (MB) constraint
const maxUploadSizeHandler = (value, rule) => {
const limit = parseType(rule.value, "number")
const maxUploadSizeHandler = (value: any, rule: UIFieldValidationRule) => {
const limit: number = parseType(rule.value, "number")
return (
value == null ||
(value?.key
? value.size / 1000000 <= limit
: value.reduce((acc, currentItem) => acc + currentItem.size, 0) /
: value.reduce(
(acc: number, currentItem: { size: number }) =>
acc + currentItem.size,
0
) /
1000000 <=
limit)
)
}
// Evaluates a min value constraint
const minValueHandler = (value, rule) => {
const minValueHandler = (value: any, rule: UIFieldValidationRule) => {
// Use same type as the value so that things can be compared
const limit = parseType(rule.value, rule.type)
return value == null || value >= limit
}
// Evaluates a max value constraint
const maxValueHandler = (value, rule) => {
const maxValueHandler = (value: any, rule: UIFieldValidationRule) => {
// Use same type as the value so that things can be compared
const limit = parseType(rule.value, rule.type)
return value == null || value <= limit
}
// Evaluates an inclusion constraint
const inclusionHandler = (value, rule) => {
return value == null || rule.value.includes(value)
const inclusionHandler = (value: any, rule: UIFieldValidationRule) => {
return value == null || (rule.value as any).includes(value)
}
// Evaluates an equal constraint
const equalHandler = (value, rule) => {
const equalHandler = (value: any, rule: UIFieldValidationRule) => {
const ruleValue = parseType(rule.value, rule.type)
return value === ruleValue
}
// Evaluates a not equal constraint
const notEqualHandler = (value, rule) => {
const notEqualHandler = (value: any, rule: UIFieldValidationRule) => {
const ruleValue = parseType(rule.value, rule.type)
if (value == null && ruleValue == null) {
return true
@ -309,7 +319,7 @@ const notEqualHandler = (value, rule) => {
}
// Evaluates a regex constraint
const regexHandler = (value, rule) => {
const regexHandler = (value: any, rule: UIFieldValidationRule) => {
const regex = parseType(rule.value, "string")
if (!value) {
value = ""
@ -318,23 +328,23 @@ const regexHandler = (value, rule) => {
}
// Evaluates a not regex constraint
const notRegexHandler = (value, rule) => {
const notRegexHandler = (value: any, rule: UIFieldValidationRule) => {
return !regexHandler(value, rule)
}
// Evaluates a contains constraint
const containsHandler = (value, rule) => {
const containsHandler = (value: any, rule: UIFieldValidationRule) => {
const expectedValue = parseType(rule.value, "string")
return value && value.includes(expectedValue)
}
// Evaluates a not contains constraint
const notContainsHandler = (value, rule) => {
const notContainsHandler = (value: any, rule: UIFieldValidationRule) => {
return !containsHandler(value, rule)
}
// Evaluates a constraint that the value must be a valid json object
const jsonHandler = value => {
const jsonHandler = (value: any) => {
if (typeof value !== "object" || Array.isArray(value)) {
return false
}
@ -372,6 +382,6 @@ const handlerMap = {
* @param value the value to test
* @returns {boolean} whether the value exists or not
*/
const exists = value => {
const exists = <T = any>(value: T | null | undefined): value is T => {
return value != null && value !== ""
}

View File

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

@ -16,7 +16,6 @@ import {
} from "@/stores"
import { get } from "svelte/store"
import { initWebsocket } from "@/websocket"
import { Readable } from "svelte/store"
import {
Screen,
Theme,
@ -27,6 +26,8 @@ import {
Snippet,
UIComponentError,
CustomComponent,
Table,
DataFetchDatasource,
} from "@budibase/types"
import { ActionTypes } from "@/constants"
import { APIClient } from "@budibase/frontend-core"
@ -75,14 +76,13 @@ declare global {
}
}
export type Context = Readable<Record<string, any>>
export interface SDK {
API: APIClient
styleable: any
Provider: any
ActionTypes: typeof ActionTypes
fetchDatasourceSchema: any
fetchDatasourceDefinition: (datasource: DataFetchDatasource) => Promise<Table>
generateGoldenSample: any
builderStore: typeof builderStore
authStore: typeof authStore

View File

@ -6,4 +6,5 @@ export type Component = Readable<{
styles: any
editing: boolean
errorState: boolean
path: string[]
}>

View File

@ -1,3 +1,7 @@
import { Readable } from "svelte/store"
export * from "./components"
export * from "./fields"
export * from "./forms"
export type Context = Readable<Record<string, any>>

View File

@ -1,7 +1,9 @@
import {
FetchOAuth2ConfigsResponse,
UpsertOAuth2ConfigRequest,
UpsertOAuth2ConfigResponse,
InsertOAuth2ConfigRequest,
InsertOAuth2ConfigResponse,
UpdateOAuth2ConfigRequest,
UpdateOAuth2ConfigResponse,
ValidateConfigRequest,
ValidateConfigResponse,
} from "@budibase/types"
@ -10,13 +12,12 @@ import { BaseAPIClient } from "./types"
export interface OAuth2Endpoints {
fetch: () => Promise<FetchOAuth2ConfigsResponse["configs"]>
create: (
config: UpsertOAuth2ConfigRequest
) => Promise<UpsertOAuth2ConfigResponse>
config: InsertOAuth2ConfigRequest
) => Promise<InsertOAuth2ConfigResponse>
update: (
id: string,
config: UpsertOAuth2ConfigRequest
) => Promise<UpsertOAuth2ConfigResponse>
delete: (id: string) => Promise<void>
config: UpdateOAuth2ConfigRequest
) => Promise<UpdateOAuth2ConfigResponse>
delete: (id: string, rev: string) => Promise<void>
validate: (config: ValidateConfigRequest) => Promise<ValidateConfigResponse>
}
@ -37,8 +38,8 @@ export const buildOAuth2Endpoints = (API: BaseAPIClient): OAuth2Endpoints => ({
*/
create: async config => {
return await API.post<
UpsertOAuth2ConfigRequest,
UpsertOAuth2ConfigResponse
InsertOAuth2ConfigRequest,
InsertOAuth2ConfigResponse
>({
url: `/api/oauth2`,
body: {
@ -50,10 +51,10 @@ export const buildOAuth2Endpoints = (API: BaseAPIClient): OAuth2Endpoints => ({
/**
* Updates an existing OAuth2 configuration.
*/
update: async (id, config) => {
return await API.put<UpsertOAuth2ConfigRequest, UpsertOAuth2ConfigResponse>(
update: async config => {
return await API.put<UpdateOAuth2ConfigRequest, UpdateOAuth2ConfigResponse>(
{
url: `/api/oauth2/${id}`,
url: `/api/oauth2/${config._id}`,
body: {
...config,
},
@ -64,10 +65,11 @@ export const buildOAuth2Endpoints = (API: BaseAPIClient): OAuth2Endpoints => ({
/**
* Deletes an OAuth2 configuration by its id.
* @param id the ID of the OAuth2 config
* @param rev the rev of the OAuth2 config
*/
delete: async id => {
delete: async (id, rev) => {
return await API.delete<void, void>({
url: `/api/oauth2/${id}`,
url: `/api/oauth2/${id}/${rev}`,
})
},
validate: async function (

View File

@ -1,14 +1,15 @@
import {
UpsertOAuth2ConfigRequest,
UpsertOAuth2ConfigResponse,
Ctx,
FetchOAuth2ConfigsResponse,
OAuth2Config,
RequiredKeys,
OAuth2ConfigResponse,
PASSWORD_REPLACEMENT,
ValidateConfigResponse,
ValidateConfigRequest,
InsertOAuth2ConfigRequest,
InsertOAuth2ConfigResponse,
UpdateOAuth2ConfigRequest,
UpdateOAuth2ConfigResponse,
} from "@budibase/types"
import sdk from "../../sdk"
@ -16,7 +17,8 @@ function toFetchOAuth2ConfigsResponse(
config: OAuth2Config
): OAuth2ConfigResponse {
return {
id: config.id,
_id: config._id!,
_rev: config._rev!,
name: config.name,
url: config.url,
clientId: config.clientId,
@ -43,10 +45,10 @@ export async function fetch(ctx: Ctx<void, FetchOAuth2ConfigsResponse>) {
}
export async function create(
ctx: Ctx<UpsertOAuth2ConfigRequest, UpsertOAuth2ConfigResponse>
ctx: Ctx<InsertOAuth2ConfigRequest, InsertOAuth2ConfigResponse>
) {
const { body } = ctx.request
const newConfig: RequiredKeys<Omit<OAuth2Config, "id">> = {
const newConfig = {
name: body.name,
url: body.url,
clientId: body.clientId,
@ -62,11 +64,17 @@ export async function create(
}
export async function edit(
ctx: Ctx<UpsertOAuth2ConfigRequest, UpsertOAuth2ConfigResponse>
ctx: Ctx<UpdateOAuth2ConfigRequest, UpdateOAuth2ConfigResponse>
) {
const { body } = ctx.request
const toUpdate: RequiredKeys<OAuth2Config> = {
id: ctx.params.id,
if (ctx.params.id !== body._id) {
ctx.throw("Path and body ids do not match", 400)
}
const toUpdate = {
_id: body._id,
_rev: body._rev,
name: body.name,
url: body.url,
clientId: body.clientId,
@ -80,12 +88,10 @@ export async function edit(
}
}
export async function remove(
ctx: Ctx<UpsertOAuth2ConfigRequest, UpsertOAuth2ConfigResponse>
) {
const configToRemove = ctx.params.id
export async function remove(ctx: Ctx<void, void>) {
const { id, rev } = ctx.params
await sdk.oauth2.remove(configToRemove)
await sdk.oauth2.remove(id, rev)
ctx.status = 204
}
@ -100,10 +106,10 @@ export async function validate(
method: body.method,
}
if (config.clientSecret === PASSWORD_REPLACEMENT && body.id) {
const existingConfig = await sdk.oauth2.get(body.id)
if (config.clientSecret === PASSWORD_REPLACEMENT && body._id) {
const existingConfig = await sdk.oauth2.get(body._id)
if (!existingConfig) {
ctx.throw(`OAuth2 config with id '${body.id}' not found.`, 404)
ctx.throw(`OAuth2 config with id '${body._id}' not found.`, 404)
}
config.clientSecret = existingConfig.clientSecret

View File

@ -6,7 +6,7 @@ import authorized from "../../middleware/authorized"
import * as controller from "../controllers/oauth2"
import Joi from "joi"
const baseValidation = {
const baseSchema = {
url: Joi.string().required(),
clientId: Joi.string().required(),
clientSecret: Joi.string().required(),
@ -15,24 +15,27 @@ const baseValidation = {
.valid(...Object.values(OAuth2CredentialsMethod)),
}
function oAuth2ConfigValidator() {
return middleware.joiValidator.body(
Joi.object({
name: Joi.string().required(),
...baseValidation,
}),
{ allowUnknown: false }
)
}
const insertSchema = Joi.object({
name: Joi.string().required(),
...baseSchema,
})
function oAuth2ConfigValidationValidator() {
return middleware.joiValidator.body(
Joi.object({
id: Joi.string(),
...baseValidation,
}),
{ allowUnknown: false }
)
const updateSchema = Joi.object({
_id: Joi.string().required(),
_rev: Joi.string().required(),
name: Joi.string().required(),
...baseSchema,
})
const validationSchema = Joi.object({
_id: Joi.string(),
...baseSchema,
})
function oAuth2ConfigValidator(
schema: typeof validationSchema | typeof insertSchema | typeof updateSchema
) {
return middleware.joiValidator.body(schema, { allowUnknown: false })
}
const router: Router = new Router()
@ -41,24 +44,24 @@ router.get("/api/oauth2", authorized(PermissionType.BUILDER), controller.fetch)
router.post(
"/api/oauth2",
authorized(PermissionType.BUILDER),
oAuth2ConfigValidator(),
oAuth2ConfigValidator(insertSchema),
controller.create
)
router.put(
"/api/oauth2/:id",
authorized(PermissionType.BUILDER),
oAuth2ConfigValidator(),
oAuth2ConfigValidator(updateSchema),
controller.edit
)
router.delete(
"/api/oauth2/:id",
"/api/oauth2/:id/:rev",
authorized(PermissionType.BUILDER),
controller.remove
)
router.post(
"/api/oauth2/validate",
authorized(PermissionType.BUILDER),
oAuth2ConfigValidationValidator(),
oAuth2ConfigValidator(validationSchema),
controller.validate
)

View File

@ -1,9 +1,9 @@
import {
OAuth2Config,
DocumentType,
InsertOAuth2ConfigRequest,
OAuth2ConfigResponse,
OAuth2CredentialsMethod,
PASSWORD_REPLACEMENT,
UpsertOAuth2ConfigRequest,
VirtualDocumentType,
} from "@budibase/types"
import * as setup from "./utilities"
import { generator } from "@budibase/backend-core/tests"
@ -12,7 +12,7 @@ import _ from "lodash/fp"
describe("/oauth2", () => {
let config = setup.getConfig()
function makeOAuth2Config(): UpsertOAuth2ConfigRequest {
function makeOAuth2Config(): InsertOAuth2ConfigRequest {
return {
name: generator.guid(),
url: generator.url(),
@ -27,7 +27,7 @@ describe("/oauth2", () => {
beforeEach(async () => await config.newTenant())
const expectOAuth2ConfigId = expect.stringMatching(
`^${VirtualDocumentType.OAUTH2_CONFIG}_.+$`
`^${DocumentType.OAUTH2_CONFIG}_.+$`
)
describe("fetch", () => {
@ -43,7 +43,7 @@ describe("/oauth2", () => {
for (let i = 0; i < 10; i++) {
const oauth2Config = makeOAuth2Config()
const result = await config.api.oauth2.create(oauth2Config)
existingConfigs.push({ ...oauth2Config, id: result.config.id })
existingConfigs.push(result.config)
}
const response = await config.api.oauth2.fetch()
@ -51,7 +51,8 @@ describe("/oauth2", () => {
expect(response).toEqual({
configs: expect.arrayContaining(
existingConfigs.map(c => ({
id: c.id,
_id: c._id,
_rev: c._rev,
name: c.name,
url: c.url,
clientId: c.clientId,
@ -72,7 +73,8 @@ describe("/oauth2", () => {
expect(response).toEqual({
configs: [
{
id: expectOAuth2ConfigId,
_id: expectOAuth2ConfigId,
_rev: expect.stringMatching(/^1-\w+/),
name: oauth2Config.name,
url: oauth2Config.url,
clientId: oauth2Config.clientId,
@ -90,25 +92,29 @@ describe("/oauth2", () => {
await config.api.oauth2.create(oauth2Config2, { status: 201 })
const response = await config.api.oauth2.fetch()
expect(response.configs).toEqual([
{
id: expectOAuth2ConfigId,
name: oauth2Config.name,
url: oauth2Config.url,
clientId: oauth2Config.clientId,
clientSecret: PASSWORD_REPLACEMENT,
method: oauth2Config.method,
},
{
id: expectOAuth2ConfigId,
name: oauth2Config2.name,
url: oauth2Config2.url,
clientId: oauth2Config2.clientId,
clientSecret: PASSWORD_REPLACEMENT,
method: oauth2Config2.method,
},
])
expect(response.configs[0].id).not.toEqual(response.configs[1].id)
expect(response.configs).toEqual(
expect.arrayContaining([
{
_id: expectOAuth2ConfigId,
_rev: expect.stringMatching(/^1-\w+/),
name: oauth2Config.name,
url: oauth2Config.url,
clientId: oauth2Config.clientId,
clientSecret: PASSWORD_REPLACEMENT,
method: oauth2Config.method,
},
{
_id: expectOAuth2ConfigId,
_rev: expect.stringMatching(/^1-\w+/),
name: oauth2Config2.name,
url: oauth2Config2.url,
clientId: oauth2Config2.clientId,
clientSecret: PASSWORD_REPLACEMENT,
method: oauth2Config2.method,
},
])
)
expect(response.configs[0]._id).not.toEqual(response.configs[1]._id)
})
it("cannot create configurations with already existing names", async () => {
@ -118,7 +124,7 @@ describe("/oauth2", () => {
await config.api.oauth2.create(oauth2Config2, {
status: 400,
body: {
message: "Name already used",
message: `OAuth2 config with name '${oauth2Config.name}' is already taken.`,
status: 400,
},
})
@ -126,7 +132,8 @@ describe("/oauth2", () => {
const response = await config.api.oauth2.fetch()
expect(response.configs).toEqual([
{
id: expectOAuth2ConfigId,
_id: expectOAuth2ConfigId,
_rev: expect.stringMatching(/^1-\w+/),
name: oauth2Config.name,
url: oauth2Config.url,
clientId: oauth2Config.clientId,
@ -138,7 +145,7 @@ describe("/oauth2", () => {
})
describe("update", () => {
let existingConfigs: OAuth2Config[] = []
let existingConfigs: OAuth2ConfigResponse[] = []
beforeEach(async () => {
existingConfigs = []
@ -146,14 +153,14 @@ describe("/oauth2", () => {
const oauth2Config = makeOAuth2Config()
const result = await config.api.oauth2.create(oauth2Config)
existingConfigs.push({ ...oauth2Config, id: result.config.id })
existingConfigs.push(result.config)
}
})
it("can update an existing configuration", async () => {
const { id: configId, ...configData } = _.sample(existingConfigs)!
const configData = _.sample(existingConfigs)!
await config.api.oauth2.update(configId, {
await config.api.oauth2.update({
...configData,
name: "updated name",
})
@ -163,7 +170,8 @@ describe("/oauth2", () => {
expect(response.configs).toEqual(
expect.arrayContaining([
{
id: configId,
_id: configData._id,
_rev: expect.not.stringMatching(configData._rev),
name: "updated name",
url: configData.url,
clientId: configData.clientId,
@ -175,7 +183,12 @@ describe("/oauth2", () => {
})
it("throw if config not found", async () => {
await config.api.oauth2.update("unexisting", makeOAuth2Config(), {
const toUpdate = {
...makeOAuth2Config(),
_id: "unexisting",
_rev: "unexisting",
}
await config.api.oauth2.update(toUpdate, {
status: 404,
body: { message: "OAuth2 config with id 'unexisting' not found." },
})
@ -183,12 +196,10 @@ describe("/oauth2", () => {
it("throws if trying to use an existing name", async () => {
const [config1, config2] = _.sampleSize(2, existingConfigs)
const { id: configId, ...configData } = config1
await config.api.oauth2.update(
configId,
{
...configData,
...config1,
name: config2.name,
},
{
@ -202,7 +213,7 @@ describe("/oauth2", () => {
})
describe("delete", () => {
let existingConfigs: OAuth2Config[] = []
let existingConfigs: OAuth2ConfigResponse[] = []
beforeEach(async () => {
existingConfigs = []
@ -210,22 +221,26 @@ describe("/oauth2", () => {
const oauth2Config = makeOAuth2Config()
const result = await config.api.oauth2.create(oauth2Config)
existingConfigs.push({ ...oauth2Config, id: result.config.id })
existingConfigs.push(result.config)
}
})
it("can delete an existing configuration", async () => {
const { id: configId } = _.sample(existingConfigs)!
const configToDelete = _.sample(existingConfigs)!
await config.api.oauth2.delete(configId, { status: 204 })
await config.api.oauth2.delete(configToDelete._id, configToDelete._rev, {
status: 204,
})
const response = await config.api.oauth2.fetch()
expect(response.configs).toHaveLength(existingConfigs.length - 1)
expect(response.configs.find(c => c.id === configId)).toBeUndefined()
expect(
response.configs.find(c => c._id === configToDelete._id)
).toBeUndefined()
})
it("throw if config not found", async () => {
await config.api.oauth2.delete("unexisting", {
await config.api.oauth2.delete("unexisting", "rev", {
status: 404,
body: { message: "OAuth2 config with id 'unexisting' not found." },
})

View File

@ -307,7 +307,7 @@ describe("REST Integration", () => {
config.appId,
async () =>
await integration.read({
authConfigId: oauthConfig.id,
authConfigId: oauthConfig._id,
authConfigType: RestAuthType.OAUTH2,
})
)
@ -349,7 +349,7 @@ describe("REST Integration", () => {
config.appId,
async () =>
await integration.read({
authConfigId: oauthConfig.id,
authConfigId: oauthConfig._id,
authConfigType: RestAuthType.OAUTH2,
})
)

View File

@ -1,104 +1,95 @@
import { context, docIds, HTTPError, utils } from "@budibase/backend-core"
import {
Database,
DocumentType,
OAuth2Config,
OAuth2Configs,
PASSWORD_REPLACEMENT,
SEPARATOR,
VirtualDocumentType,
WithRequired,
} from "@budibase/types"
async function getDocument(db: Database = context.getAppDB()) {
const result = await db.tryGet<OAuth2Configs>(DocumentType.OAUTH2_CONFIG)
return result
}
type CreatedOAuthConfig = WithRequired<OAuth2Config, "_id" | "_rev">
export async function fetch(): Promise<OAuth2Config[]> {
const result = await getDocument()
if (!result) {
return []
}
return Object.values(result.configs)
}
async function guardName(name: string, id?: string) {
const existingConfigs = await fetch()
export async function create(
config: Omit<OAuth2Config, "id">
): Promise<OAuth2Config> {
const db = context.getAppDB()
const doc: OAuth2Configs = (await getDocument(db)) ?? {
_id: DocumentType.OAUTH2_CONFIG,
configs: {},
}
if (Object.values(doc.configs).find(c => c.name === config.name)) {
throw new HTTPError("Name already used", 400)
}
const id = `${VirtualDocumentType.OAUTH2_CONFIG}${SEPARATOR}${utils.newid()}`
doc.configs[id] = {
id,
...config,
}
await db.put(doc)
return doc.configs[id]
}
export async function get(id: string): Promise<OAuth2Config | undefined> {
const doc = await getDocument()
return doc?.configs?.[id]
}
export async function update(config: OAuth2Config): Promise<OAuth2Config> {
const db = context.getAppDB()
const doc: OAuth2Configs = (await getDocument(db)) ?? {
_id: DocumentType.OAUTH2_CONFIG,
configs: {},
}
if (!doc.configs[config.id]) {
throw new HTTPError(`OAuth2 config with id '${config.id}' not found.`, 404)
}
if (
Object.values(doc.configs).find(
c => c.name === config.name && c.id !== config.id
)
) {
if (existingConfigs.find(c => c.name === name && c._id !== id)) {
throw new HTTPError(
`OAuth2 config with name '${config.name}' is already taken.`,
`OAuth2 config with name '${name}' is already taken.`,
400
)
}
}
doc.configs[config.id] = {
export async function fetch(): Promise<CreatedOAuthConfig[]> {
const db = context.getAppDB()
const docs = await db.allDocs<OAuth2Config>(
docIds.getOAuth2ConfigParams(null, { include_docs: true })
)
const result = docs.rows.map(r => ({
...r.doc!,
_id: r.doc!._id!,
_rev: r.doc!._rev!,
}))
return result
}
export async function create(
config: Omit<OAuth2Config, "_id" | "_rev" | "createdAt" | "updatedAt">
): Promise<CreatedOAuthConfig> {
const db = context.getAppDB()
await guardName(config.name)
const response = await db.put({
_id: `${DocumentType.OAUTH2_CONFIG}${SEPARATOR}${utils.newid()}`,
...config,
})
return {
_id: response.id!,
_rev: response.rev!,
...config,
}
}
export async function get(id: string): Promise<OAuth2Config | undefined> {
const db = context.getAppDB()
return await db.tryGet(id)
}
export async function update(
config: CreatedOAuthConfig
): Promise<CreatedOAuthConfig> {
const db = context.getAppDB()
await guardName(config.name, config._id)
const existing = await get(config._id)
if (!existing) {
throw new HTTPError(`OAuth2 config with id '${config._id}' not found.`, 404)
}
const toUpdate = {
...config,
clientSecret:
config.clientSecret === PASSWORD_REPLACEMENT
? doc.configs[config.id].clientSecret
? existing.clientSecret
: config.clientSecret,
}
await db.put(doc)
return doc.configs[config.id]
const result = await db.put(toUpdate)
return { ...toUpdate, _rev: result.rev }
}
export async function remove(configId: string): Promise<void> {
export async function remove(configId: string, _rev: string): Promise<void> {
const db = context.getAppDB()
const doc: OAuth2Configs = (await getDocument(db)) ?? {
_id: DocumentType.OAUTH2_CONFIG,
configs: {},
try {
await db.remove(configId, _rev)
} catch (e: any) {
if (e.status === 404) {
throw new HTTPError(`OAuth2 config with id '${configId}' not found.`, 404)
}
throw e
}
if (!doc.configs[configId]) {
throw new HTTPError(`OAuth2 config with id '${configId}' not found.`, 404)
}
delete doc.configs[configId]
await db.put(doc)
const usageLog = await db.tryGet(docIds.generateOAuth2LogID(configId))
if (usageLog) {
await db.remove(usageLog)

View File

@ -57,7 +57,7 @@ describe("oauth2 utils", () => {
method,
})
const response = await getToken(oauthConfig.id)
const response = await getToken(oauthConfig._id)
return response
})
@ -75,7 +75,7 @@ describe("oauth2 utils", () => {
method,
})
await getToken(oauthConfig.id)
await getToken(oauthConfig._id)
})
).rejects.toThrow("Error fetching oauth2 token: Not Found")
})
@ -91,7 +91,7 @@ describe("oauth2 utils", () => {
method,
})
await getToken(oauthConfig.id)
await getToken(oauthConfig._id)
})
).rejects.toThrow(
"Error fetching oauth2 token: Invalid client or Invalid client credentials"
@ -109,7 +109,7 @@ describe("oauth2 utils", () => {
method,
})
await getToken(oauthConfig.id)
await getToken(oauthConfig._id)
})
).rejects.toThrow(
"Error fetching oauth2 token: Invalid client or Invalid client credentials"

View File

@ -1,7 +1,9 @@
import {
UpsertOAuth2ConfigRequest,
UpsertOAuth2ConfigResponse,
InsertOAuth2ConfigRequest,
InsertOAuth2ConfigResponse,
FetchOAuth2ConfigsResponse,
UpdateOAuth2ConfigRequest,
UpdateOAuth2ConfigResponse,
} from "@budibase/types"
import { Expectations, TestAPI } from "./base"
@ -13,10 +15,10 @@ export class OAuth2API extends TestAPI {
}
create = async (
body: UpsertOAuth2ConfigRequest,
body: InsertOAuth2ConfigRequest,
expectations?: Expectations
) => {
return await this._post<UpsertOAuth2ConfigResponse>("/api/oauth2", {
return await this._post<InsertOAuth2ConfigResponse>("/api/oauth2", {
body,
expectations: {
status: expectations?.status ?? 201,
@ -26,18 +28,20 @@ export class OAuth2API extends TestAPI {
}
update = async (
id: string,
body: UpsertOAuth2ConfigRequest,
body: UpdateOAuth2ConfigRequest,
expectations?: Expectations
) => {
return await this._put<UpsertOAuth2ConfigResponse>(`/api/oauth2/${id}`, {
body,
expectations,
})
return await this._put<UpdateOAuth2ConfigResponse>(
`/api/oauth2/${body._id}`,
{
body,
expectations,
}
)
}
delete = async (id: string, expectations?: Expectations) => {
return await this._delete<void>(`/api/oauth2/${id}`, {
delete = async (id: string, rev: string, expectations?: Expectations) => {
return await this._delete<void>(`/api/oauth2/${id}/${rev}`, {
expectations,
})
}

View File

@ -10,7 +10,7 @@ import { User } from "@budibase/types"
* @param key the key
* @return the value or null if a value was not found for this key
*/
export const deepGet = (obj: { [x: string]: any }, key: string) => {
export const deepGet = (obj: Record<string, any> | undefined, key: string) => {
if (!obj || !key) {
return null
}

View File

@ -1,7 +1,8 @@
import { OAuth2CredentialsMethod } from "@budibase/types"
export interface OAuth2ConfigResponse {
id: string
_id: string
_rev: string
name: string
url: string
clientId: string
@ -13,7 +14,7 @@ export interface FetchOAuth2ConfigsResponse {
configs: (OAuth2ConfigResponse & { lastUsage?: string })[]
}
export interface UpsertOAuth2ConfigRequest {
export interface InsertOAuth2ConfigRequest {
name: string
url: string
clientId: string
@ -21,12 +22,26 @@ export interface UpsertOAuth2ConfigRequest {
method: OAuth2CredentialsMethod
}
export interface UpsertOAuth2ConfigResponse {
export interface InsertOAuth2ConfigResponse {
config: OAuth2ConfigResponse
}
export interface UpdateOAuth2ConfigRequest {
_id: string
_rev: string
name: string
url: string
clientId: string
clientSecret: string
method: OAuth2CredentialsMethod
}
export interface UpdateOAuth2ConfigResponse {
config: OAuth2ConfigResponse
}
export interface ValidateConfigRequest {
id?: string
_id?: string
url: string
clientId: string
clientSecret: string

View File

@ -5,15 +5,10 @@ export enum OAuth2CredentialsMethod {
BODY = "BODY",
}
export interface OAuth2Config {
id: string
export interface OAuth2Config extends Document {
name: string
url: string
clientId: string
clientSecret: string
method: OAuth2CredentialsMethod
}
export interface OAuth2Configs extends Document {
configs: Record<string, OAuth2Config>
}

View File

@ -83,7 +83,6 @@ export enum InternalTable {
export enum VirtualDocumentType {
VIEW = "view",
ROW_ACTION = "row_action",
OAUTH2_CONFIG = "oauth2",
}
// Because VirtualDocumentTypes can overlap, we need to make sure that we search

View File

@ -3,7 +3,7 @@ import {
FieldConstraints,
type FieldSchema,
type FormulaResponseType,
} from "../"
} from "../../"
export interface UIField {
name: string

View File

@ -0,0 +1,2 @@
export * from "./fields"
export * from "./validationRules"

View File

@ -0,0 +1,25 @@
import { FieldType } from "../../documents"
export interface UIFieldValidationRule {
type: `${FieldType}`
constraint: FieldValidationRuleType
value?: string | number | string[]
error: string
}
export type FieldValidationRuleType =
| "required"
| "minLength"
| "maxLength"
| "minValue"
| "maxValue"
| "inclusion"
| "equal"
| "notEqual"
| "regex"
| "notRegex"
| "contains"
| "notContains"
| "json"
| "maxFileSize"
| "maxUploadSize"