diff --git a/packages/entities/entities-plugins/src/components/PluginForm.vue b/packages/entities/entities-plugins/src/components/PluginForm.vue index 9b01227ef4..642e5047ff 100644 --- a/packages/entities/entities-plugins/src/components/PluginForm.vue +++ b/packages/entities/entities-plugins/src/components/PluginForm.vue @@ -166,6 +166,7 @@ import { type PluginFormFields, type PluginFormState, type PluginOrdering, + type CustomSchemas, } from '../types' import PluginEntityForm from './PluginEntityForm.vue' import PluginFormActionsWrapper from './PluginFormActionsWrapper.vue' @@ -502,7 +503,7 @@ const getArrayType = (list: unknown[]): string => { const buildFormSchema = (parentKey: string, response: Record, initialFormSchema: Record, arrayNested?: boolean) => { let schema = (response && response.fields) || [] - const pluginSchema = customSchemas[props.pluginType as keyof typeof customSchemas] + const pluginSchema = customSchemas[props.pluginType as keyof CustomSchemas] const credentialSchema = CREDENTIAL_METADATA[props.pluginType]?.schema?.fields // schema can either be an object or an array of objects. If it's an array, convert it to an object @@ -1005,9 +1006,9 @@ const initScopeFields = (): void => { } // apply custom front-end schema if overwriteDefault is true - if (customSchemas[props.pluginType as keyof typeof customSchemas] && customSchemas[props.pluginType as keyof typeof customSchemas].overwriteDefault) { - if (Object.hasOwnProperty.call(customSchemas[props.pluginType as keyof typeof customSchemas], 'formSchema')) { - Object.assign(defaultFormSchema, customSchemas[props.pluginType as keyof typeof customSchemas].formSchema) + if (customSchemas[props.pluginType as keyof CustomSchemas] && customSchemas[props.pluginType as keyof CustomSchemas].overwriteDefault) { + if (Object.hasOwnProperty.call(customSchemas[props.pluginType as keyof CustomSchemas], 'formSchema')) { + Object.assign(defaultFormSchema, customSchemas[props.pluginType as keyof CustomSchemas].formSchema) } } } @@ -1176,21 +1177,31 @@ const saveFormData = async (): Promise => { let response: AxiosResponse | undefined + const payload = JSON.parse(JSON.stringify(getRequestBody.value)) + const customSchema = customSchemas[props.pluginType as keyof CustomSchemas] + if (typeof customSchema?.shamefullyTransformPayload === 'function') { + customSchema.shamefullyTransformPayload({ + originalModel: formFieldsOriginal, + model: form.fields, + payload, + }) + } + // TODO: determine validate URL for credentials // don't validate custom plugins if (!treatAsCredential.value && !isCustomPlugin.value) { - await axiosInstance.post(validateSubmitUrl.value, getRequestBody.value) + await axiosInstance.post(validateSubmitUrl.value, payload) } if (formType.value === 'create') { - response = await axiosInstance.post(submitUrl.value, getRequestBody.value) + response = await axiosInstance.post(submitUrl.value, payload) } else if (formType.value === 'edit') { response = props.config.app === 'konnect' // Note 1: Konnect currently uses PUT because PATCH is not fully supported in Koko // If this changes, the `edit` form methods should be re-evaluated/updated accordingly // Note 2: Because Konnect uses PUT, we need to include dynamic ordering in the request body - ? await axiosInstance.put(submitUrl.value, Object.assign({ ordering: dynamicOrdering.value }, getRequestBody.value)) - : await axiosInstance.patch(submitUrl.value, getRequestBody.value) + ? await axiosInstance.put(submitUrl.value, Object.assign({ ordering: dynamicOrdering.value }, payload)) + : await axiosInstance.patch(submitUrl.value, payload) } // Set initial state of `formFieldsOriginal` to these values in order to detect changes diff --git a/packages/entities/entities-plugins/src/composables/useSchemas.ts b/packages/entities/entities-plugins/src/composables/useSchemas.ts index a2969334df..7ffeaf9d60 100644 --- a/packages/entities/entities-plugins/src/composables/useSchemas.ts +++ b/packages/entities/entities-plugins/src/composables/useSchemas.ts @@ -4,6 +4,7 @@ import { customFields, getSharedFormName } from '@kong-ui-public/forms' import { PLUGIN_METADATA } from '../definitions/metadata' import { aiPromptDecoratorSchema } from '../definitions/schemas/AIPromptDecorator' import { aiPromptTemplateSchema } from '../definitions/schemas/AIPromptTemplate' +import { aiProxyAdvancedSchema } from '../definitions/schemas/AIProxyAdvanced' import { aiRateLimitingAdvancedSchema } from '../definitions/schemas/AIRateLimitingAdvanced' import { applicationRegistrationSchema } from '../definitions/schemas/ApplicationRegistration' import { ArrayInputFieldSchema } from '../definitions/schemas/ArrayInputFieldSchema' @@ -127,6 +128,10 @@ export const useSchemas = (options?: UseSchemasOptions) => { ...aiPromptTemplateSchema, }, + 'ai-proxy-advanced': { + ...aiProxyAdvancedSchema, + }, + 'ai-rate-limiting-advanced': { ...aiRateLimitingAdvancedSchema, }, diff --git a/packages/entities/entities-plugins/src/definitions/schemas/AIProxyAdvanced.ts b/packages/entities/entities-plugins/src/definitions/schemas/AIProxyAdvanced.ts new file mode 100644 index 0000000000..d923baa570 --- /dev/null +++ b/packages/entities/entities-plugins/src/definitions/schemas/AIProxyAdvanced.ts @@ -0,0 +1,38 @@ +import type { CommonSchemaFields } from '../../types/plugins/shared' + +export const aiProxyAdvancedSchema: CommonSchemaFields = { + // For the ai-proxy-advanced plugin, 'config-embeddings' and 'config-vectordb' fields are non-required + // but they have nested fields that are required. If the nested fields are not provided, 'config-embeddings' + // and 'config-vectordb' should be set to null in the payload. + shamefullyTransformPayload: ({ originalModel, model, payload }) => { + const isDirty = (key: string) => { + // if the model value is the same as the original value + // the field is not dirty + if (originalModel[key] === model[key]) { + return false + } + + // if the original value is null, and the model value is not empty or NaN + // the field is dirty + if (originalModel[key] === null) { + return model[key] !== '' && !Number.isNaN(model[key]) + } + + // if the original value is not null, and the model value is not the same as the original value + // the field is dirty + return true + } + + const hasDirtyFields = (prefix: string) => Object.keys(originalModel) + .some(key => key.startsWith(prefix) && isDirty(key)) + + // If 'config-embeddings-*' fields are not dirty, set 'config-embeddings' to null in the payload + if (!hasDirtyFields('config-embeddings')) { + payload.config.embeddings = null + } + // If 'config-vectordb-*' fields are not dirty, set 'config-vectordb' to null in the payload + if (!hasDirtyFields('config-vectordb')) { + payload.config.vectordb = null + } + }, +} diff --git a/packages/entities/entities-plugins/src/types/plugin-form.ts b/packages/entities/entities-plugins/src/types/plugin-form.ts index 0e93a41bd3..b14c95a09d 100644 --- a/packages/entities/entities-plugins/src/types/plugin-form.ts +++ b/packages/entities/entities-plugins/src/types/plugin-form.ts @@ -200,6 +200,7 @@ export interface CustomSchemas { 'route-by-header': RouteByHeaderSchema 'ai-prompt-decorator': AIPromptDecoratorSchema 'ai-prompt-template': AIPromptTemplateSchema + 'ai-proxy-advanced': CommonSchemaFields 'ai-rate-limiting-advanced': AIRateLimitingAdvancedSchema 'vault-auth': VaultAuthSchema 'graphql-rate-limiting-advanced': GraphQLRateLimitingAdvancedSchema diff --git a/packages/entities/entities-plugins/src/types/plugins/shared.d.ts b/packages/entities/entities-plugins/src/types/plugins/shared.d.ts index 9c093075d4..399fe1031d 100644 --- a/packages/entities/entities-plugins/src/types/plugins/shared.d.ts +++ b/packages/entities/entities-plugins/src/types/plugins/shared.d.ts @@ -44,4 +44,5 @@ export interface CommonSchemaFields { id?: string overwriteDefault?: boolean formSchema?: Record + shamefullyTransformPayload?: (params: { payload: Record } & Record) => void }