Merge branch 'BUDI-9127/split-oauthconfig-per-document' into BUDI-9127/track-usage
This commit is contained in:
commit
f904646519
|
@ -200,3 +200,13 @@ export function getStartEndKeyURL(baseKey: any, tenantId?: string) {
|
||||||
export const getPluginParams = (pluginId?: string | null, otherProps = {}) => {
|
export const getPluginParams = (pluginId?: string | null, otherProps = {}) => {
|
||||||
return getDocParams(DocumentType.PLUGIN, pluginId, 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)
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import Popover from "../Popover/Popover.svelte"
|
import Popover from "../Popover/Popover.svelte"
|
||||||
import Layout from "../Layout/Layout.svelte"
|
import Layout from "../Layout/Layout.svelte"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
@ -11,15 +11,17 @@
|
||||||
getThemeClassNames,
|
getThemeClassNames,
|
||||||
DefaultAppTheme,
|
DefaultAppTheme,
|
||||||
} from "@budibase/shared-core"
|
} from "@budibase/shared-core"
|
||||||
|
import type { Theme } from "@budibase/types"
|
||||||
|
import type { PopoverAlignment } from "../constants"
|
||||||
|
|
||||||
export let value
|
export let value: string | undefined = undefined
|
||||||
export let size = "M"
|
export let size: "S" | "M" | "L" = "M"
|
||||||
export let spectrumTheme
|
export let spectrumTheme: Theme | undefined = undefined
|
||||||
export let offset
|
export let offset: number | undefined = undefined
|
||||||
export let align
|
export let align: PopoverAlignment | undefined = undefined
|
||||||
|
|
||||||
let dropdown
|
let dropdown: Popover | undefined
|
||||||
let preview
|
let preview: HTMLElement | undefined
|
||||||
|
|
||||||
$: customValue = getCustomValue(value)
|
$: customValue = getCustomValue(value)
|
||||||
$: checkColor = getCheckColor(value)
|
$: checkColor = getCheckColor(value)
|
||||||
|
@ -124,7 +126,7 @@
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const getThemeClasses = theme => {
|
const getThemeClasses = (theme: Theme | undefined) => {
|
||||||
if (!theme) {
|
if (!theme) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -132,12 +134,12 @@
|
||||||
return getThemeClassNames(theme)
|
return getThemeClassNames(theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onChange = value => {
|
const onChange = (value: string | null) => {
|
||||||
dispatch("change", value)
|
dispatch("change", value)
|
||||||
dropdown.hide()
|
dropdown?.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCustomValue = value => {
|
const getCustomValue = (value: string | undefined) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
@ -152,11 +154,11 @@
|
||||||
return found ? null : value
|
return found ? null : value
|
||||||
}
|
}
|
||||||
|
|
||||||
const prettyPrint = color => {
|
const prettyPrint = (color: string) => {
|
||||||
return capitalise(color.split("-").join(" "))
|
return capitalise(color.split("-").join(" "))
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCheckColor = value => {
|
const getCheckColor = (value: string | undefined) => {
|
||||||
// Use dynamic color for theme grays
|
// Use dynamic color for theme grays
|
||||||
if (value?.includes("-gray-")) {
|
if (value?.includes("-gray-")) {
|
||||||
return /^.*(gray-(50|75|100|200|300|400|500))\)$/.test(value)
|
return /^.*(gray-(50|75|100|200|300|400|500))\)$/.test(value)
|
||||||
|
@ -187,7 +189,7 @@
|
||||||
bind:this={preview}
|
bind:this={preview}
|
||||||
class="preview size--{size || 'M'}"
|
class="preview size--{size || 'M'}"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
dropdown.toggle()
|
dropdown?.toggle()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -88,16 +88,14 @@
|
||||||
// Required for reactivity
|
// Required for reactivity
|
||||||
fields = fields
|
fields = fields
|
||||||
const newActivity = {}
|
const newActivity = {}
|
||||||
const trimmedFields = []
|
|
||||||
for (let idx = 0; idx < fields.length; idx++) {
|
for (let idx = 0; idx < fields.length; idx++) {
|
||||||
const fieldName = fields[idx].name
|
const fieldName = fields[idx].name
|
||||||
if (fieldName) {
|
if (fieldName) {
|
||||||
newActivity[fieldName] = fieldActivity[idx]
|
newActivity[fieldName] = fieldActivity[idx]
|
||||||
trimmedFields.push(fields[idx])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
activity = newActivity
|
activity = newActivity
|
||||||
dispatch("change", trimmedFields)
|
dispatch("change", fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isJsonArray(value) {
|
function isJsonArray(value) {
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
...authConfigs,
|
...authConfigs,
|
||||||
...$oauth2.configs.map(c => ({
|
...$oauth2.configs.map(c => ({
|
||||||
label: c.name,
|
label: c.name,
|
||||||
value: c.id,
|
value: c._id,
|
||||||
})),
|
})),
|
||||||
]
|
]
|
||||||
$: authConfig = allConfigs.find(c => c.value === authConfigId)
|
$: authConfig = allConfigs.find(c => c.value === authConfigId)
|
||||||
|
@ -108,8 +108,9 @@
|
||||||
{#each $oauth2.configs as config}
|
{#each $oauth2.configs as config}
|
||||||
<ListItem
|
<ListItem
|
||||||
title={config.name}
|
title={config.name}
|
||||||
on:click={() => selectConfiguration(config.id, RestAuthType.OAUTH2)}
|
on:click={() =>
|
||||||
selected={config.id === authConfigId}
|
selectConfiguration(config._id, RestAuthType.OAUTH2)}
|
||||||
|
selected={config._id === authConfigId}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</List>
|
</List>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
Modal,
|
Modal,
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import type { OAuth2Config } from "@budibase/types"
|
import type { OAuth2Config } from "@/types"
|
||||||
import OAuth2ConfigModalContent from "./OAuth2ConfigModalContent.svelte"
|
import OAuth2ConfigModalContent from "./OAuth2ConfigModalContent.svelte"
|
||||||
import { confirm } from "@/helpers"
|
import { confirm } from "@/helpers"
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@
|
||||||
warning: true,
|
warning: true,
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
try {
|
try {
|
||||||
await oauth2.delete(row.id)
|
await oauth2.delete(row._id, row._rev)
|
||||||
notifications.success(`Config '${row.name}' deleted successfully`)
|
notifications.success(`Config '${row.name}' deleted successfully`)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
let message = "Error deleting config"
|
let message = "Error deleting config"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { oauth2 } from "@/stores/builder"
|
import { oauth2 } from "@/stores/builder"
|
||||||
import type { OAuth2Config, UpsertOAuth2Config } from "@/types"
|
import type { OAuth2Config } from "@/types"
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Divider,
|
Divider,
|
||||||
|
@ -12,6 +12,7 @@
|
||||||
notifications,
|
notifications,
|
||||||
Select,
|
Select,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
|
import type { InsertOAuth2ConfigRequest } from "@budibase/types"
|
||||||
import {
|
import {
|
||||||
OAuth2CredentialsMethod,
|
OAuth2CredentialsMethod,
|
||||||
PASSWORD_REPLACEMENT,
|
PASSWORD_REPLACEMENT,
|
||||||
|
@ -50,7 +51,7 @@
|
||||||
name: requiredString("Name is required.").refine(
|
name: requiredString("Name is required.").refine(
|
||||||
val =>
|
val =>
|
||||||
!$oauth2.configs
|
!$oauth2.configs
|
||||||
.filter(c => c.id !== config.id)
|
.filter(c => c._id !== config._id)
|
||||||
.map(c => c.name.toLowerCase())
|
.map(c => c.name.toLowerCase())
|
||||||
.includes(val.toLowerCase()),
|
.includes(val.toLowerCase()),
|
||||||
{
|
{
|
||||||
|
@ -63,7 +64,7 @@
|
||||||
method: z.nativeEnum(OAuth2CredentialsMethod, {
|
method: z.nativeEnum(OAuth2CredentialsMethod, {
|
||||||
message: "Authentication method is required.",
|
message: "Authentication method is required.",
|
||||||
}),
|
}),
|
||||||
}) satisfies ZodType<UpsertOAuth2Config>
|
}) satisfies ZodType<InsertOAuth2ConfigRequest>
|
||||||
|
|
||||||
const validationResult = validator.safeParse(config)
|
const validationResult = validator.safeParse(config)
|
||||||
errors = {}
|
errors = {}
|
||||||
|
@ -91,7 +92,7 @@
|
||||||
const { data: configData } = validationResult
|
const { data: configData } = validationResult
|
||||||
try {
|
try {
|
||||||
const connectionValidation = await oauth2.validate({
|
const connectionValidation = await oauth2.validate({
|
||||||
id: config?.id,
|
_id: config?._id,
|
||||||
url: configData.url,
|
url: configData.url,
|
||||||
clientId: configData.clientId,
|
clientId: configData.clientId,
|
||||||
clientSecret: configData.clientSecret,
|
clientSecret: configData.clientSecret,
|
||||||
|
@ -110,7 +111,11 @@
|
||||||
await oauth2.create(configData)
|
await oauth2.create(configData)
|
||||||
notifications.success("Settings created.")
|
notifications.success("Settings created.")
|
||||||
} else {
|
} else {
|
||||||
await oauth2.edit(config!.id, configData)
|
await oauth2.edit({
|
||||||
|
...configData,
|
||||||
|
_id: config!._id,
|
||||||
|
_rev: config!._rev,
|
||||||
|
})
|
||||||
notifications.success("Settings saved.")
|
notifications.success("Settings saved.")
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
import { API } from "@/api"
|
import { API } from "@/api"
|
||||||
import { BudiStore } from "@/stores/BudiStore"
|
import { BudiStore } from "@/stores/BudiStore"
|
||||||
import { OAuth2Config, UpsertOAuth2Config } from "@/types"
|
import { OAuth2Config } from "@/types"
|
||||||
import { RequiredKeys, ValidateConfigRequest } from "@budibase/types"
|
import {
|
||||||
|
InsertOAuth2ConfigRequest,
|
||||||
|
UpdateOAuth2ConfigRequest,
|
||||||
|
ValidateConfigRequest,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
interface OAuth2StoreState {
|
interface OAuth2StoreState {
|
||||||
configs: OAuth2Config[]
|
configs: OAuth2Config[]
|
||||||
|
@ -26,8 +30,9 @@ export class OAuth2Store extends BudiStore<OAuth2StoreState> {
|
||||||
const configs = await API.oauth2.fetch()
|
const configs = await API.oauth2.fetch()
|
||||||
this.store.update(store => ({
|
this.store.update(store => ({
|
||||||
...store,
|
...store,
|
||||||
configs: configs.map<RequiredKeys<OAuth2Config>>(c => ({
|
configs: configs.map(c => ({
|
||||||
id: c.id,
|
_id: c._id,
|
||||||
|
_rev: c._rev,
|
||||||
name: c.name,
|
name: c.name,
|
||||||
url: c.url,
|
url: c.url,
|
||||||
clientId: c.clientId,
|
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 API.oauth2.create(config)
|
||||||
await this.fetch()
|
await this.fetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
async edit(id: string, config: UpsertOAuth2Config) {
|
async edit(config: UpdateOAuth2ConfigRequest) {
|
||||||
await API.oauth2.update(id, config)
|
await API.oauth2.update(config)
|
||||||
await this.fetch()
|
await this.fetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id: string) {
|
async delete(id: string, rev: string) {
|
||||||
await API.oauth2.delete(id)
|
await API.oauth2.delete(id, rev)
|
||||||
await this.fetch()
|
await this.fetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,3 @@
|
||||||
import {
|
import { OAuth2ConfigResponse } from "@budibase/types"
|
||||||
UpsertOAuth2ConfigRequest,
|
|
||||||
FetchOAuth2ConfigsResponse,
|
|
||||||
} from "@budibase/types"
|
|
||||||
|
|
||||||
export type OAuth2Config = FetchOAuth2ConfigsResponse["configs"][0]
|
export interface OAuth2Config extends OAuth2ConfigResponse {}
|
||||||
|
|
||||||
export interface UpsertOAuth2Config extends UpsertOAuth2ConfigRequest {}
|
|
||||||
|
|
|
@ -1,38 +1,39 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import InnerForm from "./InnerForm.svelte"
|
import InnerForm from "./InnerForm.svelte"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
|
import type { DataFetchDatasource, Table, TableSchema } from "@budibase/types"
|
||||||
|
|
||||||
export let dataSource
|
export let dataSource: DataFetchDatasource
|
||||||
export let size
|
export let size: "Medium" | "Large"
|
||||||
export let disabled = false
|
export let disabled: boolean = false
|
||||||
export let readonly = false
|
export let readonly: boolean = false
|
||||||
export let actionType = "Create"
|
export let actionType: "Create" = "Create"
|
||||||
export let initialFormStep = 1
|
export let initialFormStep: string | number = 1
|
||||||
|
|
||||||
// Not exposed as a builder setting. Used internally to disable validation
|
// Not exposed as a builder setting. Used internally to disable validation
|
||||||
// for fields rendered in things like search blocks.
|
// 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
|
// Not exposed as a builder setting. Used internally to allow searching on
|
||||||
// auto columns.
|
// auto columns.
|
||||||
export let editAutoColumns = false
|
export let editAutoColumns: boolean = false
|
||||||
|
|
||||||
const context = getContext("context")
|
const context = getContext("context")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const { fetchDatasourceSchema, fetchDatasourceDefinition } = getContext("sdk")
|
const { fetchDatasourceSchema, fetchDatasourceDefinition } = getContext("sdk")
|
||||||
|
|
||||||
const getInitialFormStep = () => {
|
const getInitialFormStep = () => {
|
||||||
const parsedFormStep = parseInt(initialFormStep)
|
const parsedFormStep = parseInt(initialFormStep.toString())
|
||||||
if (isNaN(parsedFormStep)) {
|
if (isNaN(parsedFormStep)) {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
return parsedFormStep
|
return parsedFormStep
|
||||||
}
|
}
|
||||||
|
|
||||||
let definition
|
let definition: Table | undefined
|
||||||
let schema
|
let schema: TableSchema | undefined
|
||||||
let loaded = false
|
let loaded = false
|
||||||
let currentStep = getContext("current-step") || writable(getInitialFormStep())
|
let currentStep = getContext("current-step") || writable(getInitialFormStep())
|
||||||
|
|
||||||
|
@ -49,7 +50,12 @@
|
||||||
)
|
)
|
||||||
|
|
||||||
// Returns the closes data context which isn't a built in context
|
// 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
|
// Only inherit values for update forms
|
||||||
if (type !== "Update") {
|
if (type !== "Update") {
|
||||||
return {}
|
return {}
|
||||||
|
@ -82,11 +88,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetches the form schema from this form's dataSource
|
// Fetches the form schema from this form's dataSource
|
||||||
const fetchSchema = async dataSource => {
|
const fetchSchema = async (dataSource: DataFetchDatasource) => {
|
||||||
try {
|
try {
|
||||||
definition = await fetchDatasourceDefinition(dataSource)
|
definition = await fetchDatasourceDefinition(dataSource)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
definition = null
|
definition = undefined
|
||||||
}
|
}
|
||||||
const res = await fetchDatasourceSchema(dataSource)
|
const res = await fetchDatasourceSchema(dataSource)
|
||||||
schema = res || {}
|
schema = res || {}
|
||||||
|
@ -98,7 +104,7 @@
|
||||||
// Generates a predictable string that uniquely identifies a schema. We can't
|
// Generates a predictable string that uniquely identifies a schema. We can't
|
||||||
// simply stringify the whole schema as there are array fields which have
|
// simply stringify the whole schema as there are array fields which have
|
||||||
// random order.
|
// random order.
|
||||||
const generateSchemaKey = schema => {
|
const generateSchemaKey = (schema: TableSchema | undefined) => {
|
||||||
if (!schema) {
|
if (!schema) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +1,62 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { setContext, getContext } from "svelte"
|
import { setContext, getContext } from "svelte"
|
||||||
|
import type { Readable, Writable } from "svelte/store"
|
||||||
import { derived, get, writable } from "svelte/store"
|
import { derived, get, writable } from "svelte/store"
|
||||||
import { createValidatorFromConstraints } from "./validation"
|
import { createValidatorFromConstraints } from "./validation"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
|
import type {
|
||||||
|
DataFetchDatasource,
|
||||||
|
FieldSchema,
|
||||||
|
FieldType,
|
||||||
|
Table,
|
||||||
|
TableSchema,
|
||||||
|
UIFieldValidationRule,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
export let dataSource = undefined
|
type FieldInfo<T = any> = {
|
||||||
export let disabled = false
|
name: string
|
||||||
export let readonly = false
|
step: number
|
||||||
export let initialValues = undefined
|
type: `${FieldType}`
|
||||||
export let size = undefined
|
fieldState: {
|
||||||
export let schema = undefined
|
fieldId: string
|
||||||
export let definition = undefined
|
value: T
|
||||||
export let disableSchemaValidation = false
|
defaultValue: T
|
||||||
export let editAutoColumns = false
|
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
|
// For internal use only, to disable context when being used with standalone
|
||||||
// fields
|
// 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
|
// We export this store so that when we remount the inner form we can still
|
||||||
// persist what step we're on
|
// persist what step we're on
|
||||||
export let currentStep
|
export let currentStep: Writable<number>
|
||||||
|
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const { styleable, Provider, ActionTypes } = getContext("sdk")
|
const { styleable, Provider, ActionTypes } = getContext("sdk")
|
||||||
|
|
||||||
let fields = []
|
let fields: Writable<FieldInfo>[] = []
|
||||||
const formState = writable({
|
const formState = writable({
|
||||||
values: {},
|
values: {},
|
||||||
errors: {},
|
errors: {},
|
||||||
|
@ -75,19 +106,24 @@
|
||||||
|
|
||||||
// Generates a derived store from an array of fields, comprised of a map of
|
// Generates a derived store from an array of fields, comprised of a map of
|
||||||
// extracted values from the field array
|
// extracted values from the field array
|
||||||
const deriveFieldProperty = (fieldStores, getProp) => {
|
const deriveFieldProperty = (
|
||||||
|
fieldStores: Readable<FieldInfo>[],
|
||||||
|
getProp: (_field: FieldInfo) => any
|
||||||
|
) => {
|
||||||
return derived(fieldStores, fieldValues => {
|
return derived(fieldStores, fieldValues => {
|
||||||
const reducer = (map, field) => ({ ...map, [field.name]: getProp(field) })
|
return fieldValues.reduce(
|
||||||
return fieldValues.reduce(reducer, {})
|
(map, field) => ({ ...map, [field.name]: getProp(field) }),
|
||||||
|
{}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Derives any enrichments which need to be made so that bindings work for
|
// Derives any enrichments which need to be made so that bindings work for
|
||||||
// special data types like attachments. Relationships are currently not
|
// special data types like attachments. Relationships are currently not
|
||||||
// handled as we don't have the primaryDisplay field that is required.
|
// handled as we don't have the primaryDisplay field that is required.
|
||||||
const deriveBindingEnrichments = fieldStores => {
|
const deriveBindingEnrichments = (fieldStores: Readable<FieldInfo>[]) => {
|
||||||
return derived(fieldStores, fieldValues => {
|
return derived(fieldStores, fieldValues => {
|
||||||
let enrichments = {}
|
const enrichments: Record<string, string> = {}
|
||||||
fieldValues.forEach(field => {
|
fieldValues.forEach(field => {
|
||||||
if (field.type === "attachment") {
|
if (field.type === "attachment") {
|
||||||
const value = field.fieldState.value
|
const value = field.fieldState.value
|
||||||
|
@ -104,7 +140,11 @@
|
||||||
|
|
||||||
// Derive the overall form value and deeply set all field paths so that we
|
// Derive the overall form value and deeply set all field paths so that we
|
||||||
// can support things like JSON fields.
|
// 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 || {})
|
let formValue = Helpers.cloneDeep(initialValues || {})
|
||||||
|
|
||||||
// We need to sort the keys to avoid a JSON field overwriting a nested field
|
// We need to sort the keys to avoid a JSON field overwriting a nested field
|
||||||
|
@ -118,7 +158,7 @@
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
return a.lastUpdate > b.lastUpdate
|
return a.lastUpdate - b.lastUpdate
|
||||||
})
|
})
|
||||||
|
|
||||||
// Merge all values and enrichments into a single value
|
// Merge all values and enrichments into a single value
|
||||||
|
@ -132,12 +172,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Searches the field array for a certain field
|
// Searches the field array for a certain field
|
||||||
const getField = name => {
|
const getField = (name: string) => {
|
||||||
return fields.find(field => get(field).name === name)
|
return fields.find(field => get(field).name === name)!
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanitises a value by ensuring it doesn't contain any invalid data
|
// 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
|
// Check arrays - remove any values not present in the field schema and
|
||||||
// convert any values supplied to strings
|
// convert any values supplied to strings
|
||||||
if (Array.isArray(value) && type === "array" && schema) {
|
if (Array.isArray(value) && type === "array" && schema) {
|
||||||
|
@ -149,13 +193,13 @@
|
||||||
|
|
||||||
const formApi = {
|
const formApi = {
|
||||||
registerField: (
|
registerField: (
|
||||||
field,
|
field: string,
|
||||||
type,
|
type: FieldType,
|
||||||
defaultValue = null,
|
defaultValue: string | null = null,
|
||||||
fieldDisabled = false,
|
fieldDisabled: boolean = false,
|
||||||
fieldReadOnly = false,
|
fieldReadOnly: boolean = false,
|
||||||
validationRules,
|
validationRules: UIFieldValidationRule[],
|
||||||
step = 1
|
step: number = 1
|
||||||
) => {
|
) => {
|
||||||
if (!field) {
|
if (!field) {
|
||||||
return
|
return
|
||||||
|
@ -200,7 +244,7 @@
|
||||||
const isAutoColumn = !!schema?.[field]?.autocolumn
|
const isAutoColumn = !!schema?.[field]?.autocolumn
|
||||||
|
|
||||||
// Construct field info
|
// Construct field info
|
||||||
const fieldInfo = writable({
|
const fieldInfo = writable<FieldInfo>({
|
||||||
name: field,
|
name: field,
|
||||||
type,
|
type,
|
||||||
step: step || 1,
|
step: step || 1,
|
||||||
|
@ -210,7 +254,8 @@
|
||||||
error: initialError,
|
error: initialError,
|
||||||
disabled:
|
disabled:
|
||||||
disabled || fieldDisabled || (isAutoColumn && !editAutoColumns),
|
disabled || fieldDisabled || (isAutoColumn && !editAutoColumns),
|
||||||
readonly: readonly || fieldReadOnly || schema?.[field]?.readonly,
|
readonly:
|
||||||
|
readonly || fieldReadOnly || (schema?.[field] as any)?.readonly,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
validator,
|
validator,
|
||||||
lastUpdate: Date.now(),
|
lastUpdate: Date.now(),
|
||||||
|
@ -254,7 +299,13 @@
|
||||||
get(field).fieldApi.reset()
|
get(field).fieldApi.reset()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
changeStep: ({ type, number }) => {
|
changeStep: ({
|
||||||
|
type,
|
||||||
|
number,
|
||||||
|
}: {
|
||||||
|
type: "next" | "prev" | "first" | "specific"
|
||||||
|
number: any
|
||||||
|
}) => {
|
||||||
if (type === "next") {
|
if (type === "next") {
|
||||||
currentStep.update(step => step + 1)
|
currentStep.update(step => step + 1)
|
||||||
} else if (type === "prev") {
|
} else if (type === "prev") {
|
||||||
|
@ -265,12 +316,12 @@
|
||||||
currentStep.set(parseInt(number))
|
currentStep.set(parseInt(number))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setStep: step => {
|
setStep: (step: number) => {
|
||||||
if (step) {
|
if (step) {
|
||||||
currentStep.set(step)
|
currentStep.set(step)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setFieldValue: (fieldName, value) => {
|
setFieldValue: (fieldName: string, value: any) => {
|
||||||
const field = getField(fieldName)
|
const field = getField(fieldName)
|
||||||
if (!field) {
|
if (!field) {
|
||||||
return
|
return
|
||||||
|
@ -278,7 +329,7 @@
|
||||||
const { fieldApi } = get(field)
|
const { fieldApi } = get(field)
|
||||||
fieldApi.setValue(value)
|
fieldApi.setValue(value)
|
||||||
},
|
},
|
||||||
resetField: fieldName => {
|
resetField: (fieldName: string) => {
|
||||||
const field = getField(fieldName)
|
const field = getField(fieldName)
|
||||||
if (!field) {
|
if (!field) {
|
||||||
return
|
return
|
||||||
|
@ -289,9 +340,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates an API for a specific field
|
// Creates an API for a specific field
|
||||||
const makeFieldApi = field => {
|
const makeFieldApi = (field: string) => {
|
||||||
// Sets the value for a certain field and invokes validation
|
// 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 fieldInfo = getField(field)
|
||||||
const { fieldState } = get(fieldInfo)
|
const { fieldState } = get(fieldInfo)
|
||||||
const { validator } = fieldState
|
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
|
// We don't want to actually remove the field state when deregistering, just
|
||||||
// remove any errors and validation
|
// remove any errors and validation
|
||||||
const deregister = () => {
|
const deregister = () => {
|
||||||
|
@ -370,7 +391,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updates the disabled state of a certain field
|
// Updates the disabled state of a certain field
|
||||||
const setDisabled = fieldDisabled => {
|
const setDisabled = (fieldDisabled: boolean) => {
|
||||||
const fieldInfo = getField(field)
|
const fieldInfo = getField(field)
|
||||||
|
|
||||||
// Auto columns are always disabled
|
// Auto columns are always disabled
|
||||||
|
@ -386,7 +407,6 @@
|
||||||
return {
|
return {
|
||||||
setValue,
|
setValue,
|
||||||
reset,
|
reset,
|
||||||
updateValidation,
|
|
||||||
setDisabled,
|
setDisabled,
|
||||||
deregister,
|
deregister,
|
||||||
validate: () => {
|
validate: () => {
|
||||||
|
@ -412,7 +432,15 @@
|
||||||
// register their fields to step 1
|
// register their fields to step 1
|
||||||
setContext("form-step", writable(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") {
|
if (type === "set") {
|
||||||
formApi.setFieldValue(field, value)
|
formApi.setFieldValue(field, value)
|
||||||
} else {
|
} else {
|
||||||
|
@ -420,16 +448,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleScrollToField = ({ field }) => {
|
const handleScrollToField = (props: { field: FieldInfo | string }) => {
|
||||||
if (!field.fieldState) {
|
let field
|
||||||
field = get(getField(field))
|
if (typeof props.field === "string") {
|
||||||
|
field = get(getField(props.field))
|
||||||
|
} else {
|
||||||
|
field = props.field
|
||||||
}
|
}
|
||||||
const fieldId = field.fieldState.fieldId
|
const fieldId = field.fieldState.fieldId
|
||||||
const fieldElement = document.getElementById(fieldId)
|
const fieldElement = document.getElementById(fieldId)
|
||||||
if (fieldElement) {
|
if (fieldElement) {
|
||||||
fieldElement.focus({ preventScroll: true })
|
fieldElement.focus({ preventScroll: true })
|
||||||
}
|
}
|
||||||
const label = document.querySelector(`label[for="${fieldId}"]`)
|
const label = document.querySelector<HTMLElement>(`label[for="${fieldId}"]`)
|
||||||
if (label) {
|
if (label) {
|
||||||
label.style.scrollMargin = "100px"
|
label.style.scrollMargin = "100px"
|
||||||
label.scrollIntoView({ behavior: "smooth", block: "nearest" })
|
label.scrollIntoView({ behavior: "smooth", block: "nearest" })
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import { FieldTypes } from "../../../constants"
|
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
|
import {
|
||||||
|
FieldConstraints,
|
||||||
|
FieldType,
|
||||||
|
Table,
|
||||||
|
UIFieldValidationRule,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a validation function from a combination of schema-level constraints
|
* 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
|
* @returns {function} a validator function which accepts test values
|
||||||
*/
|
*/
|
||||||
export const createValidatorFromConstraints = (
|
export const createValidatorFromConstraints = (
|
||||||
schemaConstraints,
|
schemaConstraints: FieldConstraints | null | undefined,
|
||||||
customRules,
|
customRules: UIFieldValidationRule[],
|
||||||
field,
|
field: string,
|
||||||
definition
|
definition: Table | undefined
|
||||||
) => {
|
) => {
|
||||||
let rules = []
|
let rules: UIFieldValidationRule[] = []
|
||||||
|
|
||||||
// Convert schema constraints into validation rules
|
// Convert schema constraints into validation rules
|
||||||
if (schemaConstraints) {
|
if (schemaConstraints) {
|
||||||
// Required constraint
|
// Required constraint
|
||||||
if (
|
if (
|
||||||
field === definition?.primaryDisplay ||
|
field === definition?.primaryDisplay ||
|
||||||
schemaConstraints.presence?.allowEmpty === false ||
|
(schemaConstraints.presence as any)?.allowEmpty === false ||
|
||||||
schemaConstraints.presence === true
|
schemaConstraints.presence === true
|
||||||
) {
|
) {
|
||||||
rules.push({
|
rules.push({
|
||||||
|
@ -106,7 +111,7 @@ export const createValidatorFromConstraints = (
|
||||||
rules = rules.concat(customRules || [])
|
rules = rules.concat(customRules || [])
|
||||||
|
|
||||||
// Evaluate each constraint
|
// Evaluate each constraint
|
||||||
return value => {
|
return (value: any) => {
|
||||||
for (let rule of rules) {
|
for (let rule of rules) {
|
||||||
const error = evaluateRule(rule, value)
|
const error = evaluateRule(rule, value)
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -124,7 +129,7 @@ export const createValidatorFromConstraints = (
|
||||||
* @param value the value to validate against
|
* @param value the value to validate against
|
||||||
* @returns {null|*} an error if validation fails or null if it passes
|
* @returns {null|*} an error if validation fails or null if it passes
|
||||||
*/
|
*/
|
||||||
const evaluateRule = (rule, value) => {
|
const evaluateRule = (rule: UIFieldValidationRule, value: any) => {
|
||||||
if (!rule) {
|
if (!rule) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -150,14 +155,14 @@ const evaluateRule = (rule, value) => {
|
||||||
* @param type the type to parse
|
* @param type the type to parse
|
||||||
* @returns {boolean|string|*|number|null|array} the parsed value, or null if invalid
|
* @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
|
// Treat nulls or empty strings as null
|
||||||
if (!exists(value) || !type) {
|
if (!exists(value) || !type) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse as string
|
// Parse as string
|
||||||
if (type === FieldTypes.STRING) {
|
if (type === FieldType.STRING) {
|
||||||
if (typeof value === "string" || Array.isArray(value)) {
|
if (typeof value === "string" || Array.isArray(value)) {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
@ -168,7 +173,7 @@ const parseType = (value, type) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse as number
|
// Parse as number
|
||||||
if (type === FieldTypes.NUMBER) {
|
if (type === FieldType.NUMBER) {
|
||||||
if (isNaN(value)) {
|
if (isNaN(value)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -176,7 +181,7 @@ const parseType = (value, type) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse as date
|
// Parse as date
|
||||||
if (type === FieldTypes.DATETIME) {
|
if (type === FieldType.DATETIME) {
|
||||||
if (value instanceof Date) {
|
if (value instanceof Date) {
|
||||||
return value.getTime()
|
return value.getTime()
|
||||||
}
|
}
|
||||||
|
@ -185,7 +190,7 @@ const parseType = (value, type) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse as boolean
|
// Parse as boolean
|
||||||
if (type === FieldTypes.BOOLEAN) {
|
if (type === FieldType.BOOLEAN) {
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
return value.toLowerCase() === "true"
|
return value.toLowerCase() === "true"
|
||||||
}
|
}
|
||||||
|
@ -193,7 +198,7 @@ const parseType = (value, type) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse attachments, treating no elements as null
|
// Parse attachments, treating no elements as null
|
||||||
if (type === FieldTypes.ATTACHMENTS) {
|
if (type === FieldType.ATTACHMENTS) {
|
||||||
if (!Array.isArray(value) || !value.length) {
|
if (!Array.isArray(value) || !value.length) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -202,8 +207,8 @@ const parseType = (value, type) => {
|
||||||
|
|
||||||
// Parse attachment/signature single, treating no key as null
|
// Parse attachment/signature single, treating no key as null
|
||||||
if (
|
if (
|
||||||
type === FieldTypes.ATTACHMENT_SINGLE ||
|
type === FieldType.ATTACHMENT_SINGLE ||
|
||||||
type === FieldTypes.SIGNATURE_SINGLE
|
type === FieldType.SIGNATURE_SINGLE
|
||||||
) {
|
) {
|
||||||
if (!value?.key) {
|
if (!value?.key) {
|
||||||
return null
|
return null
|
||||||
|
@ -212,7 +217,7 @@ const parseType = (value, type) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse links, treating no elements as null
|
// Parse links, treating no elements as null
|
||||||
if (type === FieldTypes.LINK) {
|
if (type === FieldType.LINK) {
|
||||||
if (!Array.isArray(value) || !value.length) {
|
if (!Array.isArray(value) || !value.length) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -220,7 +225,7 @@ const parseType = (value, type) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse array, treating no elements as null
|
// Parse array, treating no elements as null
|
||||||
if (type === FieldTypes.ARRAY) {
|
if (type === FieldType.ARRAY) {
|
||||||
if (!Array.isArray(value) || !value.length) {
|
if (!Array.isArray(value) || !value.length) {
|
||||||
return null
|
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
|
// For JSON we don't touch the value at all as we want to verify it in its
|
||||||
// raw form
|
// raw form
|
||||||
if (type === FieldTypes.JSON) {
|
if (type === FieldType.JSON) {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,69 +243,74 @@ const parseType = (value, type) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluates a required constraint
|
// Evaluates a required constraint
|
||||||
const requiredHandler = value => {
|
const requiredHandler = (value: any) => {
|
||||||
return value != null
|
return value != null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluates a min length constraint
|
// Evaluates a min length constraint
|
||||||
const minLengthHandler = (value, rule) => {
|
const minLengthHandler = (value: any, rule: UIFieldValidationRule) => {
|
||||||
const limit = parseType(rule.value, "number")
|
const limit = parseType(rule.value, "number")
|
||||||
return value == null || value.length >= limit
|
return value == null || value.length >= limit
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluates a max length constraint
|
// Evaluates a max length constraint
|
||||||
const maxLengthHandler = (value, rule) => {
|
const maxLengthHandler = (value: any, rule: UIFieldValidationRule) => {
|
||||||
const limit = parseType(rule.value, "number")
|
const limit = parseType(rule.value, "number")
|
||||||
return value == null || value.length <= limit
|
return value == null || value.length <= limit
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluates a max file size (MB) constraint
|
// Evaluates a max file size (MB) constraint
|
||||||
const maxFileSizeHandler = (value, rule) => {
|
const maxFileSizeHandler = (value: any, rule: UIFieldValidationRule) => {
|
||||||
const limit = parseType(rule.value, "number")
|
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))
|
return value == null || !(value?.key ? check(value) : value.some(check))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluates a max total upload size (MB) constraint
|
// Evaluates a max total upload size (MB) constraint
|
||||||
const maxUploadSizeHandler = (value, rule) => {
|
const maxUploadSizeHandler = (value: any, rule: UIFieldValidationRule) => {
|
||||||
const limit = parseType(rule.value, "number")
|
const limit: number = parseType(rule.value, "number")
|
||||||
return (
|
return (
|
||||||
value == null ||
|
value == null ||
|
||||||
(value?.key
|
(value?.key
|
||||||
? value.size / 1000000 <= limit
|
? value.size / 1000000 <= limit
|
||||||
: value.reduce((acc, currentItem) => acc + currentItem.size, 0) /
|
: value.reduce(
|
||||||
|
(acc: number, currentItem: { size: number }) =>
|
||||||
|
acc + currentItem.size,
|
||||||
|
0
|
||||||
|
) /
|
||||||
1000000 <=
|
1000000 <=
|
||||||
limit)
|
limit)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluates a min value constraint
|
// 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
|
// Use same type as the value so that things can be compared
|
||||||
const limit = parseType(rule.value, rule.type)
|
const limit = parseType(rule.value, rule.type)
|
||||||
return value == null || value >= limit
|
return value == null || value >= limit
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluates a max value constraint
|
// 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
|
// Use same type as the value so that things can be compared
|
||||||
const limit = parseType(rule.value, rule.type)
|
const limit = parseType(rule.value, rule.type)
|
||||||
return value == null || value <= limit
|
return value == null || value <= limit
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluates an inclusion constraint
|
// Evaluates an inclusion constraint
|
||||||
const inclusionHandler = (value, rule) => {
|
const inclusionHandler = (value: any, rule: UIFieldValidationRule) => {
|
||||||
return value == null || rule.value.includes(value)
|
return value == null || (rule.value as any).includes(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluates an equal constraint
|
// Evaluates an equal constraint
|
||||||
const equalHandler = (value, rule) => {
|
const equalHandler = (value: any, rule: UIFieldValidationRule) => {
|
||||||
const ruleValue = parseType(rule.value, rule.type)
|
const ruleValue = parseType(rule.value, rule.type)
|
||||||
return value === ruleValue
|
return value === ruleValue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluates a not equal constraint
|
// Evaluates a not equal constraint
|
||||||
const notEqualHandler = (value, rule) => {
|
const notEqualHandler = (value: any, rule: UIFieldValidationRule) => {
|
||||||
const ruleValue = parseType(rule.value, rule.type)
|
const ruleValue = parseType(rule.value, rule.type)
|
||||||
if (value == null && ruleValue == null) {
|
if (value == null && ruleValue == null) {
|
||||||
return true
|
return true
|
||||||
|
@ -309,7 +319,7 @@ const notEqualHandler = (value, rule) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluates a regex constraint
|
// Evaluates a regex constraint
|
||||||
const regexHandler = (value, rule) => {
|
const regexHandler = (value: any, rule: UIFieldValidationRule) => {
|
||||||
const regex = parseType(rule.value, "string")
|
const regex = parseType(rule.value, "string")
|
||||||
if (!value) {
|
if (!value) {
|
||||||
value = ""
|
value = ""
|
||||||
|
@ -318,23 +328,23 @@ const regexHandler = (value, rule) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluates a not regex constraint
|
// Evaluates a not regex constraint
|
||||||
const notRegexHandler = (value, rule) => {
|
const notRegexHandler = (value: any, rule: UIFieldValidationRule) => {
|
||||||
return !regexHandler(value, rule)
|
return !regexHandler(value, rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluates a contains constraint
|
// Evaluates a contains constraint
|
||||||
const containsHandler = (value, rule) => {
|
const containsHandler = (value: any, rule: UIFieldValidationRule) => {
|
||||||
const expectedValue = parseType(rule.value, "string")
|
const expectedValue = parseType(rule.value, "string")
|
||||||
return value && value.includes(expectedValue)
|
return value && value.includes(expectedValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluates a not contains constraint
|
// Evaluates a not contains constraint
|
||||||
const notContainsHandler = (value, rule) => {
|
const notContainsHandler = (value: any, rule: UIFieldValidationRule) => {
|
||||||
return !containsHandler(value, rule)
|
return !containsHandler(value, rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluates a constraint that the value must be a valid json object
|
// 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)) {
|
if (typeof value !== "object" || Array.isArray(value)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -372,6 +382,6 @@ const handlerMap = {
|
||||||
* @param value the value to test
|
* @param value the value to test
|
||||||
* @returns {boolean} whether the value exists or not
|
* @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 !== ""
|
return value != null && value !== ""
|
||||||
}
|
}
|
|
@ -1,12 +1,12 @@
|
||||||
import { Writable } from "svelte"
|
import { Writable } from "svelte"
|
||||||
import { Component, FieldGroupContext, FormContext } from "@/types"
|
import { Component, Context, FieldGroupContext, FormContext } from "@/types"
|
||||||
import { Readable } from "svelte/store"
|
|
||||||
import { SDK } from "@/index.ts"
|
import { SDK } from "@/index.ts"
|
||||||
|
|
||||||
declare module "svelte" {
|
declare module "svelte" {
|
||||||
export function getContext(key: "sdk"): SDK
|
export function getContext(key: "sdk"): SDK
|
||||||
export function getContext(key: "component"): Component
|
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"): FormContext | undefined
|
||||||
export function getContext(key: "form-step"): Writable<number> | undefined
|
export function getContext(key: "form-step"): Writable<number> | undefined
|
||||||
export function getContext(key: "field-group"): FieldGroupContext | undefined
|
export function getContext(key: "field-group"): FieldGroupContext | undefined
|
||||||
|
|
|
@ -16,7 +16,6 @@ import {
|
||||||
} from "@/stores"
|
} from "@/stores"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { initWebsocket } from "@/websocket"
|
import { initWebsocket } from "@/websocket"
|
||||||
import { Readable } from "svelte/store"
|
|
||||||
import {
|
import {
|
||||||
Screen,
|
Screen,
|
||||||
Theme,
|
Theme,
|
||||||
|
@ -27,6 +26,8 @@ import {
|
||||||
Snippet,
|
Snippet,
|
||||||
UIComponentError,
|
UIComponentError,
|
||||||
CustomComponent,
|
CustomComponent,
|
||||||
|
Table,
|
||||||
|
DataFetchDatasource,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { ActionTypes } from "@/constants"
|
import { ActionTypes } from "@/constants"
|
||||||
import { APIClient } from "@budibase/frontend-core"
|
import { APIClient } from "@budibase/frontend-core"
|
||||||
|
@ -75,14 +76,13 @@ declare global {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Context = Readable<Record<string, any>>
|
|
||||||
|
|
||||||
export interface SDK {
|
export interface SDK {
|
||||||
API: APIClient
|
API: APIClient
|
||||||
styleable: any
|
styleable: any
|
||||||
Provider: any
|
Provider: any
|
||||||
ActionTypes: typeof ActionTypes
|
ActionTypes: typeof ActionTypes
|
||||||
fetchDatasourceSchema: any
|
fetchDatasourceSchema: any
|
||||||
|
fetchDatasourceDefinition: (datasource: DataFetchDatasource) => Promise<Table>
|
||||||
generateGoldenSample: any
|
generateGoldenSample: any
|
||||||
builderStore: typeof builderStore
|
builderStore: typeof builderStore
|
||||||
authStore: typeof authStore
|
authStore: typeof authStore
|
||||||
|
|
|
@ -6,4 +6,5 @@ export type Component = Readable<{
|
||||||
styles: any
|
styles: any
|
||||||
editing: boolean
|
editing: boolean
|
||||||
errorState: boolean
|
errorState: boolean
|
||||||
|
path: string[]
|
||||||
}>
|
}>
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
import { Readable } from "svelte/store"
|
||||||
|
|
||||||
export * from "./components"
|
export * from "./components"
|
||||||
export * from "./fields"
|
export * from "./fields"
|
||||||
export * from "./forms"
|
export * from "./forms"
|
||||||
|
|
||||||
|
export type Context = Readable<Record<string, any>>
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import {
|
import {
|
||||||
FetchOAuth2ConfigsResponse,
|
FetchOAuth2ConfigsResponse,
|
||||||
UpsertOAuth2ConfigRequest,
|
InsertOAuth2ConfigRequest,
|
||||||
UpsertOAuth2ConfigResponse,
|
InsertOAuth2ConfigResponse,
|
||||||
|
UpdateOAuth2ConfigRequest,
|
||||||
|
UpdateOAuth2ConfigResponse,
|
||||||
ValidateConfigRequest,
|
ValidateConfigRequest,
|
||||||
ValidateConfigResponse,
|
ValidateConfigResponse,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
@ -10,13 +12,12 @@ import { BaseAPIClient } from "./types"
|
||||||
export interface OAuth2Endpoints {
|
export interface OAuth2Endpoints {
|
||||||
fetch: () => Promise<FetchOAuth2ConfigsResponse["configs"]>
|
fetch: () => Promise<FetchOAuth2ConfigsResponse["configs"]>
|
||||||
create: (
|
create: (
|
||||||
config: UpsertOAuth2ConfigRequest
|
config: InsertOAuth2ConfigRequest
|
||||||
) => Promise<UpsertOAuth2ConfigResponse>
|
) => Promise<InsertOAuth2ConfigResponse>
|
||||||
update: (
|
update: (
|
||||||
id: string,
|
config: UpdateOAuth2ConfigRequest
|
||||||
config: UpsertOAuth2ConfigRequest
|
) => Promise<UpdateOAuth2ConfigResponse>
|
||||||
) => Promise<UpsertOAuth2ConfigResponse>
|
delete: (id: string, rev: string) => Promise<void>
|
||||||
delete: (id: string) => Promise<void>
|
|
||||||
validate: (config: ValidateConfigRequest) => Promise<ValidateConfigResponse>
|
validate: (config: ValidateConfigRequest) => Promise<ValidateConfigResponse>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,8 +38,8 @@ export const buildOAuth2Endpoints = (API: BaseAPIClient): OAuth2Endpoints => ({
|
||||||
*/
|
*/
|
||||||
create: async config => {
|
create: async config => {
|
||||||
return await API.post<
|
return await API.post<
|
||||||
UpsertOAuth2ConfigRequest,
|
InsertOAuth2ConfigRequest,
|
||||||
UpsertOAuth2ConfigResponse
|
InsertOAuth2ConfigResponse
|
||||||
>({
|
>({
|
||||||
url: `/api/oauth2`,
|
url: `/api/oauth2`,
|
||||||
body: {
|
body: {
|
||||||
|
@ -50,10 +51,10 @@ export const buildOAuth2Endpoints = (API: BaseAPIClient): OAuth2Endpoints => ({
|
||||||
/**
|
/**
|
||||||
* Updates an existing OAuth2 configuration.
|
* Updates an existing OAuth2 configuration.
|
||||||
*/
|
*/
|
||||||
update: async (id, config) => {
|
update: async config => {
|
||||||
return await API.put<UpsertOAuth2ConfigRequest, UpsertOAuth2ConfigResponse>(
|
return await API.put<UpdateOAuth2ConfigRequest, UpdateOAuth2ConfigResponse>(
|
||||||
{
|
{
|
||||||
url: `/api/oauth2/${id}`,
|
url: `/api/oauth2/${config._id}`,
|
||||||
body: {
|
body: {
|
||||||
...config,
|
...config,
|
||||||
},
|
},
|
||||||
|
@ -64,10 +65,11 @@ export const buildOAuth2Endpoints = (API: BaseAPIClient): OAuth2Endpoints => ({
|
||||||
/**
|
/**
|
||||||
* Deletes an OAuth2 configuration by its id.
|
* Deletes an OAuth2 configuration by its id.
|
||||||
* @param id the ID of the OAuth2 config
|
* @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>({
|
return await API.delete<void, void>({
|
||||||
url: `/api/oauth2/${id}`,
|
url: `/api/oauth2/${id}/${rev}`,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
validate: async function (
|
validate: async function (
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
import {
|
import {
|
||||||
UpsertOAuth2ConfigRequest,
|
|
||||||
UpsertOAuth2ConfigResponse,
|
|
||||||
Ctx,
|
Ctx,
|
||||||
FetchOAuth2ConfigsResponse,
|
FetchOAuth2ConfigsResponse,
|
||||||
OAuth2Config,
|
OAuth2Config,
|
||||||
RequiredKeys,
|
|
||||||
OAuth2ConfigResponse,
|
OAuth2ConfigResponse,
|
||||||
PASSWORD_REPLACEMENT,
|
PASSWORD_REPLACEMENT,
|
||||||
ValidateConfigResponse,
|
ValidateConfigResponse,
|
||||||
ValidateConfigRequest,
|
ValidateConfigRequest,
|
||||||
|
InsertOAuth2ConfigRequest,
|
||||||
|
InsertOAuth2ConfigResponse,
|
||||||
|
UpdateOAuth2ConfigRequest,
|
||||||
|
UpdateOAuth2ConfigResponse,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import sdk from "../../sdk"
|
import sdk from "../../sdk"
|
||||||
|
|
||||||
|
@ -16,7 +17,8 @@ function toFetchOAuth2ConfigsResponse(
|
||||||
config: OAuth2Config
|
config: OAuth2Config
|
||||||
): OAuth2ConfigResponse {
|
): OAuth2ConfigResponse {
|
||||||
return {
|
return {
|
||||||
id: config.id,
|
_id: config._id!,
|
||||||
|
_rev: config._rev!,
|
||||||
name: config.name,
|
name: config.name,
|
||||||
url: config.url,
|
url: config.url,
|
||||||
clientId: config.clientId,
|
clientId: config.clientId,
|
||||||
|
@ -43,10 +45,10 @@ export async function fetch(ctx: Ctx<void, FetchOAuth2ConfigsResponse>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function create(
|
export async function create(
|
||||||
ctx: Ctx<UpsertOAuth2ConfigRequest, UpsertOAuth2ConfigResponse>
|
ctx: Ctx<InsertOAuth2ConfigRequest, InsertOAuth2ConfigResponse>
|
||||||
) {
|
) {
|
||||||
const { body } = ctx.request
|
const { body } = ctx.request
|
||||||
const newConfig: RequiredKeys<Omit<OAuth2Config, "id">> = {
|
const newConfig = {
|
||||||
name: body.name,
|
name: body.name,
|
||||||
url: body.url,
|
url: body.url,
|
||||||
clientId: body.clientId,
|
clientId: body.clientId,
|
||||||
|
@ -62,11 +64,17 @@ export async function create(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function edit(
|
export async function edit(
|
||||||
ctx: Ctx<UpsertOAuth2ConfigRequest, UpsertOAuth2ConfigResponse>
|
ctx: Ctx<UpdateOAuth2ConfigRequest, UpdateOAuth2ConfigResponse>
|
||||||
) {
|
) {
|
||||||
const { body } = ctx.request
|
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,
|
name: body.name,
|
||||||
url: body.url,
|
url: body.url,
|
||||||
clientId: body.clientId,
|
clientId: body.clientId,
|
||||||
|
@ -80,12 +88,10 @@ export async function edit(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function remove(
|
export async function remove(ctx: Ctx<void, void>) {
|
||||||
ctx: Ctx<UpsertOAuth2ConfigRequest, UpsertOAuth2ConfigResponse>
|
const { id, rev } = ctx.params
|
||||||
) {
|
|
||||||
const configToRemove = ctx.params.id
|
|
||||||
|
|
||||||
await sdk.oauth2.remove(configToRemove)
|
await sdk.oauth2.remove(id, rev)
|
||||||
ctx.status = 204
|
ctx.status = 204
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,10 +106,10 @@ export async function validate(
|
||||||
method: body.method,
|
method: body.method,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.clientSecret === PASSWORD_REPLACEMENT && body.id) {
|
if (config.clientSecret === PASSWORD_REPLACEMENT && body._id) {
|
||||||
const existingConfig = await sdk.oauth2.get(body.id)
|
const existingConfig = await sdk.oauth2.get(body._id)
|
||||||
if (!existingConfig) {
|
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
|
config.clientSecret = existingConfig.clientSecret
|
||||||
|
|
|
@ -6,7 +6,7 @@ import authorized from "../../middleware/authorized"
|
||||||
import * as controller from "../controllers/oauth2"
|
import * as controller from "../controllers/oauth2"
|
||||||
import Joi from "joi"
|
import Joi from "joi"
|
||||||
|
|
||||||
const baseValidation = {
|
const baseSchema = {
|
||||||
url: Joi.string().required(),
|
url: Joi.string().required(),
|
||||||
clientId: Joi.string().required(),
|
clientId: Joi.string().required(),
|
||||||
clientSecret: Joi.string().required(),
|
clientSecret: Joi.string().required(),
|
||||||
|
@ -15,24 +15,27 @@ const baseValidation = {
|
||||||
.valid(...Object.values(OAuth2CredentialsMethod)),
|
.valid(...Object.values(OAuth2CredentialsMethod)),
|
||||||
}
|
}
|
||||||
|
|
||||||
function oAuth2ConfigValidator() {
|
const insertSchema = Joi.object({
|
||||||
return middleware.joiValidator.body(
|
name: Joi.string().required(),
|
||||||
Joi.object({
|
...baseSchema,
|
||||||
name: Joi.string().required(),
|
})
|
||||||
...baseValidation,
|
|
||||||
}),
|
|
||||||
{ allowUnknown: false }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function oAuth2ConfigValidationValidator() {
|
const updateSchema = Joi.object({
|
||||||
return middleware.joiValidator.body(
|
_id: Joi.string().required(),
|
||||||
Joi.object({
|
_rev: Joi.string().required(),
|
||||||
id: Joi.string(),
|
name: Joi.string().required(),
|
||||||
...baseValidation,
|
...baseSchema,
|
||||||
}),
|
})
|
||||||
{ allowUnknown: false }
|
|
||||||
)
|
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()
|
const router: Router = new Router()
|
||||||
|
@ -41,24 +44,24 @@ router.get("/api/oauth2", authorized(PermissionType.BUILDER), controller.fetch)
|
||||||
router.post(
|
router.post(
|
||||||
"/api/oauth2",
|
"/api/oauth2",
|
||||||
authorized(PermissionType.BUILDER),
|
authorized(PermissionType.BUILDER),
|
||||||
oAuth2ConfigValidator(),
|
oAuth2ConfigValidator(insertSchema),
|
||||||
controller.create
|
controller.create
|
||||||
)
|
)
|
||||||
router.put(
|
router.put(
|
||||||
"/api/oauth2/:id",
|
"/api/oauth2/:id",
|
||||||
authorized(PermissionType.BUILDER),
|
authorized(PermissionType.BUILDER),
|
||||||
oAuth2ConfigValidator(),
|
oAuth2ConfigValidator(updateSchema),
|
||||||
controller.edit
|
controller.edit
|
||||||
)
|
)
|
||||||
router.delete(
|
router.delete(
|
||||||
"/api/oauth2/:id",
|
"/api/oauth2/:id/:rev",
|
||||||
authorized(PermissionType.BUILDER),
|
authorized(PermissionType.BUILDER),
|
||||||
controller.remove
|
controller.remove
|
||||||
)
|
)
|
||||||
router.post(
|
router.post(
|
||||||
"/api/oauth2/validate",
|
"/api/oauth2/validate",
|
||||||
authorized(PermissionType.BUILDER),
|
authorized(PermissionType.BUILDER),
|
||||||
oAuth2ConfigValidationValidator(),
|
oAuth2ConfigValidator(validationSchema),
|
||||||
controller.validate
|
controller.validate
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import {
|
import {
|
||||||
OAuth2Config,
|
DocumentType,
|
||||||
|
InsertOAuth2ConfigRequest,
|
||||||
|
OAuth2ConfigResponse,
|
||||||
OAuth2CredentialsMethod,
|
OAuth2CredentialsMethod,
|
||||||
PASSWORD_REPLACEMENT,
|
PASSWORD_REPLACEMENT,
|
||||||
UpsertOAuth2ConfigRequest,
|
|
||||||
VirtualDocumentType,
|
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import * as setup from "./utilities"
|
import * as setup from "./utilities"
|
||||||
import { generator } from "@budibase/backend-core/tests"
|
import { generator } from "@budibase/backend-core/tests"
|
||||||
|
@ -12,7 +12,7 @@ import _ from "lodash/fp"
|
||||||
describe("/oauth2", () => {
|
describe("/oauth2", () => {
|
||||||
let config = setup.getConfig()
|
let config = setup.getConfig()
|
||||||
|
|
||||||
function makeOAuth2Config(): UpsertOAuth2ConfigRequest {
|
function makeOAuth2Config(): InsertOAuth2ConfigRequest {
|
||||||
return {
|
return {
|
||||||
name: generator.guid(),
|
name: generator.guid(),
|
||||||
url: generator.url(),
|
url: generator.url(),
|
||||||
|
@ -27,7 +27,7 @@ describe("/oauth2", () => {
|
||||||
beforeEach(async () => await config.newTenant())
|
beforeEach(async () => await config.newTenant())
|
||||||
|
|
||||||
const expectOAuth2ConfigId = expect.stringMatching(
|
const expectOAuth2ConfigId = expect.stringMatching(
|
||||||
`^${VirtualDocumentType.OAUTH2_CONFIG}_.+$`
|
`^${DocumentType.OAUTH2_CONFIG}_.+$`
|
||||||
)
|
)
|
||||||
|
|
||||||
describe("fetch", () => {
|
describe("fetch", () => {
|
||||||
|
@ -43,7 +43,7 @@ describe("/oauth2", () => {
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
const oauth2Config = makeOAuth2Config()
|
const oauth2Config = makeOAuth2Config()
|
||||||
const result = await config.api.oauth2.create(oauth2Config)
|
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()
|
const response = await config.api.oauth2.fetch()
|
||||||
|
@ -51,7 +51,8 @@ describe("/oauth2", () => {
|
||||||
expect(response).toEqual({
|
expect(response).toEqual({
|
||||||
configs: expect.arrayContaining(
|
configs: expect.arrayContaining(
|
||||||
existingConfigs.map(c => ({
|
existingConfigs.map(c => ({
|
||||||
id: c.id,
|
_id: c._id,
|
||||||
|
_rev: c._rev,
|
||||||
name: c.name,
|
name: c.name,
|
||||||
url: c.url,
|
url: c.url,
|
||||||
clientId: c.clientId,
|
clientId: c.clientId,
|
||||||
|
@ -72,7 +73,8 @@ describe("/oauth2", () => {
|
||||||
expect(response).toEqual({
|
expect(response).toEqual({
|
||||||
configs: [
|
configs: [
|
||||||
{
|
{
|
||||||
id: expectOAuth2ConfigId,
|
_id: expectOAuth2ConfigId,
|
||||||
|
_rev: expect.stringMatching(/^1-\w+/),
|
||||||
name: oauth2Config.name,
|
name: oauth2Config.name,
|
||||||
url: oauth2Config.url,
|
url: oauth2Config.url,
|
||||||
clientId: oauth2Config.clientId,
|
clientId: oauth2Config.clientId,
|
||||||
|
@ -90,25 +92,29 @@ describe("/oauth2", () => {
|
||||||
await config.api.oauth2.create(oauth2Config2, { status: 201 })
|
await config.api.oauth2.create(oauth2Config2, { status: 201 })
|
||||||
|
|
||||||
const response = await config.api.oauth2.fetch()
|
const response = await config.api.oauth2.fetch()
|
||||||
expect(response.configs).toEqual([
|
expect(response.configs).toEqual(
|
||||||
{
|
expect.arrayContaining([
|
||||||
id: expectOAuth2ConfigId,
|
{
|
||||||
name: oauth2Config.name,
|
_id: expectOAuth2ConfigId,
|
||||||
url: oauth2Config.url,
|
_rev: expect.stringMatching(/^1-\w+/),
|
||||||
clientId: oauth2Config.clientId,
|
name: oauth2Config.name,
|
||||||
clientSecret: PASSWORD_REPLACEMENT,
|
url: oauth2Config.url,
|
||||||
method: oauth2Config.method,
|
clientId: oauth2Config.clientId,
|
||||||
},
|
clientSecret: PASSWORD_REPLACEMENT,
|
||||||
{
|
method: oauth2Config.method,
|
||||||
id: expectOAuth2ConfigId,
|
},
|
||||||
name: oauth2Config2.name,
|
{
|
||||||
url: oauth2Config2.url,
|
_id: expectOAuth2ConfigId,
|
||||||
clientId: oauth2Config2.clientId,
|
_rev: expect.stringMatching(/^1-\w+/),
|
||||||
clientSecret: PASSWORD_REPLACEMENT,
|
name: oauth2Config2.name,
|
||||||
method: oauth2Config2.method,
|
url: oauth2Config2.url,
|
||||||
},
|
clientId: oauth2Config2.clientId,
|
||||||
])
|
clientSecret: PASSWORD_REPLACEMENT,
|
||||||
expect(response.configs[0].id).not.toEqual(response.configs[1].id)
|
method: oauth2Config2.method,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
)
|
||||||
|
expect(response.configs[0]._id).not.toEqual(response.configs[1]._id)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("cannot create configurations with already existing names", async () => {
|
it("cannot create configurations with already existing names", async () => {
|
||||||
|
@ -118,7 +124,7 @@ describe("/oauth2", () => {
|
||||||
await config.api.oauth2.create(oauth2Config2, {
|
await config.api.oauth2.create(oauth2Config2, {
|
||||||
status: 400,
|
status: 400,
|
||||||
body: {
|
body: {
|
||||||
message: "Name already used",
|
message: `OAuth2 config with name '${oauth2Config.name}' is already taken.`,
|
||||||
status: 400,
|
status: 400,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -126,7 +132,8 @@ describe("/oauth2", () => {
|
||||||
const response = await config.api.oauth2.fetch()
|
const response = await config.api.oauth2.fetch()
|
||||||
expect(response.configs).toEqual([
|
expect(response.configs).toEqual([
|
||||||
{
|
{
|
||||||
id: expectOAuth2ConfigId,
|
_id: expectOAuth2ConfigId,
|
||||||
|
_rev: expect.stringMatching(/^1-\w+/),
|
||||||
name: oauth2Config.name,
|
name: oauth2Config.name,
|
||||||
url: oauth2Config.url,
|
url: oauth2Config.url,
|
||||||
clientId: oauth2Config.clientId,
|
clientId: oauth2Config.clientId,
|
||||||
|
@ -138,7 +145,7 @@ describe("/oauth2", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("update", () => {
|
describe("update", () => {
|
||||||
let existingConfigs: OAuth2Config[] = []
|
let existingConfigs: OAuth2ConfigResponse[] = []
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
existingConfigs = []
|
existingConfigs = []
|
||||||
|
@ -146,14 +153,14 @@ describe("/oauth2", () => {
|
||||||
const oauth2Config = makeOAuth2Config()
|
const oauth2Config = makeOAuth2Config()
|
||||||
const result = await config.api.oauth2.create(oauth2Config)
|
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 () => {
|
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,
|
...configData,
|
||||||
name: "updated name",
|
name: "updated name",
|
||||||
})
|
})
|
||||||
|
@ -163,7 +170,8 @@ describe("/oauth2", () => {
|
||||||
expect(response.configs).toEqual(
|
expect(response.configs).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
{
|
{
|
||||||
id: configId,
|
_id: configData._id,
|
||||||
|
_rev: expect.not.stringMatching(configData._rev),
|
||||||
name: "updated name",
|
name: "updated name",
|
||||||
url: configData.url,
|
url: configData.url,
|
||||||
clientId: configData.clientId,
|
clientId: configData.clientId,
|
||||||
|
@ -175,7 +183,12 @@ describe("/oauth2", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("throw if config not found", async () => {
|
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,
|
status: 404,
|
||||||
body: { message: "OAuth2 config with id 'unexisting' not found." },
|
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 () => {
|
it("throws if trying to use an existing name", async () => {
|
||||||
const [config1, config2] = _.sampleSize(2, existingConfigs)
|
const [config1, config2] = _.sampleSize(2, existingConfigs)
|
||||||
const { id: configId, ...configData } = config1
|
|
||||||
|
|
||||||
await config.api.oauth2.update(
|
await config.api.oauth2.update(
|
||||||
configId,
|
|
||||||
{
|
{
|
||||||
...configData,
|
...config1,
|
||||||
name: config2.name,
|
name: config2.name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -202,7 +213,7 @@ describe("/oauth2", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("delete", () => {
|
describe("delete", () => {
|
||||||
let existingConfigs: OAuth2Config[] = []
|
let existingConfigs: OAuth2ConfigResponse[] = []
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
existingConfigs = []
|
existingConfigs = []
|
||||||
|
@ -210,22 +221,26 @@ describe("/oauth2", () => {
|
||||||
const oauth2Config = makeOAuth2Config()
|
const oauth2Config = makeOAuth2Config()
|
||||||
const result = await config.api.oauth2.create(oauth2Config)
|
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 () => {
|
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()
|
const response = await config.api.oauth2.fetch()
|
||||||
expect(response.configs).toHaveLength(existingConfigs.length - 1)
|
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 () => {
|
it("throw if config not found", async () => {
|
||||||
await config.api.oauth2.delete("unexisting", {
|
await config.api.oauth2.delete("unexisting", "rev", {
|
||||||
status: 404,
|
status: 404,
|
||||||
body: { message: "OAuth2 config with id 'unexisting' not found." },
|
body: { message: "OAuth2 config with id 'unexisting' not found." },
|
||||||
})
|
})
|
||||||
|
|
|
@ -307,7 +307,7 @@ describe("REST Integration", () => {
|
||||||
config.appId,
|
config.appId,
|
||||||
async () =>
|
async () =>
|
||||||
await integration.read({
|
await integration.read({
|
||||||
authConfigId: oauthConfig.id,
|
authConfigId: oauthConfig._id,
|
||||||
authConfigType: RestAuthType.OAUTH2,
|
authConfigType: RestAuthType.OAUTH2,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -349,7 +349,7 @@ describe("REST Integration", () => {
|
||||||
config.appId,
|
config.appId,
|
||||||
async () =>
|
async () =>
|
||||||
await integration.read({
|
await integration.read({
|
||||||
authConfigId: oauthConfig.id,
|
authConfigId: oauthConfig._id,
|
||||||
authConfigType: RestAuthType.OAUTH2,
|
authConfigType: RestAuthType.OAUTH2,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,104 +1,95 @@
|
||||||
import { context, docIds, HTTPError, utils } from "@budibase/backend-core"
|
import { context, docIds, HTTPError, utils } from "@budibase/backend-core"
|
||||||
import {
|
import {
|
||||||
Database,
|
|
||||||
DocumentType,
|
DocumentType,
|
||||||
OAuth2Config,
|
OAuth2Config,
|
||||||
OAuth2Configs,
|
|
||||||
PASSWORD_REPLACEMENT,
|
PASSWORD_REPLACEMENT,
|
||||||
SEPARATOR,
|
SEPARATOR,
|
||||||
VirtualDocumentType,
|
WithRequired,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
async function getDocument(db: Database = context.getAppDB()) {
|
type CreatedOAuthConfig = WithRequired<OAuth2Config, "_id" | "_rev">
|
||||||
const result = await db.tryGet<OAuth2Configs>(DocumentType.OAUTH2_CONFIG)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetch(): Promise<OAuth2Config[]> {
|
async function guardName(name: string, id?: string) {
|
||||||
const result = await getDocument()
|
const existingConfigs = await fetch()
|
||||||
if (!result) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
return Object.values(result.configs)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function create(
|
if (existingConfigs.find(c => c.name === name && c._id !== id)) {
|
||||||
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
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throw new HTTPError(
|
throw new HTTPError(
|
||||||
`OAuth2 config with name '${config.name}' is already taken.`,
|
`OAuth2 config with name '${name}' is already taken.`,
|
||||||
400
|
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,
|
...config,
|
||||||
clientSecret:
|
clientSecret:
|
||||||
config.clientSecret === PASSWORD_REPLACEMENT
|
config.clientSecret === PASSWORD_REPLACEMENT
|
||||||
? doc.configs[config.id].clientSecret
|
? existing.clientSecret
|
||||||
: config.clientSecret,
|
: config.clientSecret,
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.put(doc)
|
const result = await db.put(toUpdate)
|
||||||
return doc.configs[config.id]
|
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 db = context.getAppDB()
|
||||||
const doc: OAuth2Configs = (await getDocument(db)) ?? {
|
try {
|
||||||
_id: DocumentType.OAUTH2_CONFIG,
|
await db.remove(configId, _rev)
|
||||||
configs: {},
|
} 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))
|
const usageLog = await db.tryGet(docIds.generateOAuth2LogID(configId))
|
||||||
if (usageLog) {
|
if (usageLog) {
|
||||||
await db.remove(usageLog)
|
await db.remove(usageLog)
|
||||||
|
|
|
@ -57,7 +57,7 @@ describe("oauth2 utils", () => {
|
||||||
method,
|
method,
|
||||||
})
|
})
|
||||||
|
|
||||||
const response = await getToken(oauthConfig.id)
|
const response = await getToken(oauthConfig._id)
|
||||||
return response
|
return response
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -75,7 +75,7 @@ describe("oauth2 utils", () => {
|
||||||
method,
|
method,
|
||||||
})
|
})
|
||||||
|
|
||||||
await getToken(oauthConfig.id)
|
await getToken(oauthConfig._id)
|
||||||
})
|
})
|
||||||
).rejects.toThrow("Error fetching oauth2 token: Not Found")
|
).rejects.toThrow("Error fetching oauth2 token: Not Found")
|
||||||
})
|
})
|
||||||
|
@ -91,7 +91,7 @@ describe("oauth2 utils", () => {
|
||||||
method,
|
method,
|
||||||
})
|
})
|
||||||
|
|
||||||
await getToken(oauthConfig.id)
|
await getToken(oauthConfig._id)
|
||||||
})
|
})
|
||||||
).rejects.toThrow(
|
).rejects.toThrow(
|
||||||
"Error fetching oauth2 token: Invalid client or Invalid client credentials"
|
"Error fetching oauth2 token: Invalid client or Invalid client credentials"
|
||||||
|
@ -109,7 +109,7 @@ describe("oauth2 utils", () => {
|
||||||
method,
|
method,
|
||||||
})
|
})
|
||||||
|
|
||||||
await getToken(oauthConfig.id)
|
await getToken(oauthConfig._id)
|
||||||
})
|
})
|
||||||
).rejects.toThrow(
|
).rejects.toThrow(
|
||||||
"Error fetching oauth2 token: Invalid client or Invalid client credentials"
|
"Error fetching oauth2 token: Invalid client or Invalid client credentials"
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import {
|
import {
|
||||||
UpsertOAuth2ConfigRequest,
|
InsertOAuth2ConfigRequest,
|
||||||
UpsertOAuth2ConfigResponse,
|
InsertOAuth2ConfigResponse,
|
||||||
FetchOAuth2ConfigsResponse,
|
FetchOAuth2ConfigsResponse,
|
||||||
|
UpdateOAuth2ConfigRequest,
|
||||||
|
UpdateOAuth2ConfigResponse,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { Expectations, TestAPI } from "./base"
|
import { Expectations, TestAPI } from "./base"
|
||||||
|
|
||||||
|
@ -13,10 +15,10 @@ export class OAuth2API extends TestAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
create = async (
|
create = async (
|
||||||
body: UpsertOAuth2ConfigRequest,
|
body: InsertOAuth2ConfigRequest,
|
||||||
expectations?: Expectations
|
expectations?: Expectations
|
||||||
) => {
|
) => {
|
||||||
return await this._post<UpsertOAuth2ConfigResponse>("/api/oauth2", {
|
return await this._post<InsertOAuth2ConfigResponse>("/api/oauth2", {
|
||||||
body,
|
body,
|
||||||
expectations: {
|
expectations: {
|
||||||
status: expectations?.status ?? 201,
|
status: expectations?.status ?? 201,
|
||||||
|
@ -26,18 +28,20 @@ export class OAuth2API extends TestAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
update = async (
|
update = async (
|
||||||
id: string,
|
body: UpdateOAuth2ConfigRequest,
|
||||||
body: UpsertOAuth2ConfigRequest,
|
|
||||||
expectations?: Expectations
|
expectations?: Expectations
|
||||||
) => {
|
) => {
|
||||||
return await this._put<UpsertOAuth2ConfigResponse>(`/api/oauth2/${id}`, {
|
return await this._put<UpdateOAuth2ConfigResponse>(
|
||||||
body,
|
`/api/oauth2/${body._id}`,
|
||||||
expectations,
|
{
|
||||||
})
|
body,
|
||||||
|
expectations,
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
delete = async (id: string, expectations?: Expectations) => {
|
delete = async (id: string, rev: string, expectations?: Expectations) => {
|
||||||
return await this._delete<void>(`/api/oauth2/${id}`, {
|
return await this._delete<void>(`/api/oauth2/${id}/${rev}`, {
|
||||||
expectations,
|
expectations,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { User } from "@budibase/types"
|
||||||
* @param key the key
|
* @param key the key
|
||||||
* @return the value or null if a value was not found for this 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) {
|
if (!obj || !key) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { OAuth2CredentialsMethod } from "@budibase/types"
|
import { OAuth2CredentialsMethod } from "@budibase/types"
|
||||||
|
|
||||||
export interface OAuth2ConfigResponse {
|
export interface OAuth2ConfigResponse {
|
||||||
id: string
|
_id: string
|
||||||
|
_rev: string
|
||||||
name: string
|
name: string
|
||||||
url: string
|
url: string
|
||||||
clientId: string
|
clientId: string
|
||||||
|
@ -13,7 +14,7 @@ export interface FetchOAuth2ConfigsResponse {
|
||||||
configs: (OAuth2ConfigResponse & { lastUsage?: string })[]
|
configs: (OAuth2ConfigResponse & { lastUsage?: string })[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpsertOAuth2ConfigRequest {
|
export interface InsertOAuth2ConfigRequest {
|
||||||
name: string
|
name: string
|
||||||
url: string
|
url: string
|
||||||
clientId: string
|
clientId: string
|
||||||
|
@ -21,12 +22,26 @@ export interface UpsertOAuth2ConfigRequest {
|
||||||
method: OAuth2CredentialsMethod
|
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
|
config: OAuth2ConfigResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ValidateConfigRequest {
|
export interface ValidateConfigRequest {
|
||||||
id?: string
|
_id?: string
|
||||||
url: string
|
url: string
|
||||||
clientId: string
|
clientId: string
|
||||||
clientSecret: string
|
clientSecret: string
|
||||||
|
|
|
@ -5,15 +5,10 @@ export enum OAuth2CredentialsMethod {
|
||||||
BODY = "BODY",
|
BODY = "BODY",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OAuth2Config {
|
export interface OAuth2Config extends Document {
|
||||||
id: string
|
|
||||||
name: string
|
name: string
|
||||||
url: string
|
url: string
|
||||||
clientId: string
|
clientId: string
|
||||||
clientSecret: string
|
clientSecret: string
|
||||||
method: OAuth2CredentialsMethod
|
method: OAuth2CredentialsMethod
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OAuth2Configs extends Document {
|
|
||||||
configs: Record<string, OAuth2Config>
|
|
||||||
}
|
|
||||||
|
|
|
@ -83,7 +83,6 @@ export enum InternalTable {
|
||||||
export enum VirtualDocumentType {
|
export enum VirtualDocumentType {
|
||||||
VIEW = "view",
|
VIEW = "view",
|
||||||
ROW_ACTION = "row_action",
|
ROW_ACTION = "row_action",
|
||||||
OAUTH2_CONFIG = "oauth2",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Because VirtualDocumentTypes can overlap, we need to make sure that we search
|
// Because VirtualDocumentTypes can overlap, we need to make sure that we search
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {
|
||||||
FieldConstraints,
|
FieldConstraints,
|
||||||
type FieldSchema,
|
type FieldSchema,
|
||||||
type FormulaResponseType,
|
type FormulaResponseType,
|
||||||
} from "../"
|
} from "../../"
|
||||||
|
|
||||||
export interface UIField {
|
export interface UIField {
|
||||||
name: string
|
name: string
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./fields"
|
||||||
|
export * from "./validationRules"
|
|
@ -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"
|
Loading…
Reference in New Issue